Blender-3-x--Python-脚本编程-全-
Blender 3.x Python 脚本编程(全)
原文:
zh.annas-archive.org/md5/77d9aa942e53de4085b0cb373abc73b4译者:飞龙
前言
Blender 是一款免费、开源的 3D 建模和动画应用程序。多年来它不断发展,自 3.0 版本以来,在许多方面与最先进的软件并驾齐驱。
它提供了一个 Python 应用程序编程接口(API)用于自动化任务,添加功能,并将 Blender 集成到大型制作中。
Python 是一种免费、开源的编程语言,用于快速而强大的脚本编写。它的语法类似于简化英语,自动内存管理以及易于集成,使其成为软件 API 和 3D 管道的标准。
本书通过 3D 流程的步骤进行讲解,从创建和操作对象开始,然后是动画和变形,最后以渲染作为结尾章节。尽管这个顺序是一个线性路径,但每个章节都可以独立存在,除了可能第六章和第八章,它们依赖于其直接前驱中引入的示例。
本书面向对象
本书面向希望扩展技能并学习编写脚本的 Blender 用户,希望自动化繁琐任务的导演,以及希望了解更多关于 Blender Python 架构的专业人士和爱好者。
本书涵盖内容
第一章, Python 与 Blender 的集成,教您如何在 Blender 中运行 Python 指令,以及使用外部编辑器和版本控制。
第二章, Python 实体和 API,教您如何为开发者设置选项,以及如何使用 Blender 模块和访问当前上下文和对象。
第三章, 创建您的插件,教您如何使用 Python 编写 Blender 插件,如何编写操作符,以及如何将条目添加到 Blender 菜单中。
第四章, 探索对象变换,教您在 Python 中如何处理位置、旋转和缩放,如何使用对象约束和变换矩阵,以及如何在操作符中使用输入属性。
第五章, 设计图形界面,提供了关于 Blender 用户界面如何工作的信息,如何创建和排列您自己的面板,以及如何加载自定义图标并显示用于自定义功能的按钮。
第六章, 构建我们的代码和插件,教您如何编写和分发模块化插件,如何显示插件首选项,以及如何更新模块中的更改。
第七章, 动画系统,教您如何访问和创建动画数据,如何编写过程式动作脚本,以及如何外推旋转。
第八章,动画修改器,指导您如何向动画添加非破坏性修改器,以及如何使用它们创建动画的进程式效果。
第九章,动画驱动器,教您如何设置动画通道的输入,如何使用 Python 表达式驱动动画,以及如何将振荡公式转换为动画对象。
第十章,高级和模式操作符,教您如何自定义操作符执行流程以及如何在您的操作符中响应输入事件。
第十一章,对象修改器,提供有关对象修改器、骨架和晶格如何工作以及如何设置变形对象的动画控制的信息。
第十二章,渲染和着色器,教您颜色和材质信息是如何应用到对象上的,以及这个过程是如何自动化的。
要充分利用本书
本书中的示例使用 Blender 版本 3.3 编写和测试。3.3 是一个长期支持版本,可以在大多数应用平台上找到,此外,它可以在Blender.org免费获得。
建议使用程序员文本编辑器。本书使用 Microsoft Visual Studio Code 1.70,这是一个轻量级的免费编辑器,可在大多数操作系统上使用,但任何其他编辑器也可以使用。
更多有关如何安装软件的说明可在第一章中找到。
书中包含的脚本考虑到向前兼容性而编写。在线可用的代码将更新以适应未来版本的更改。
假设您对 Blender 有一些经验,并且至少对 Python 的工作原理有基本了解,但特别努力将要求保持较低,并为本书中使用的每个概念提供解释。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Blender 3.3 | Windows, macOS, 或 Linux |
| Visual Studio Code 1.70 或更高版本 |
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将有助于您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 在github.com/PacktPublishing/Python-Scripting-in-Blender下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供包含本书中使用的截图和图表的颜色图像的 PDF 文件。您可以从这里下载:packt.link/G1mMt。
使用的约定
本书中使用了多种文本约定。
文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“fcurve.modifiers.new(type) 方法根据提供的参数类型创建一个新的修改器。它返回新的修改器。”
代码块按以下方式设置:
bl_info = {
"name": "Object Shaker", "author": "Packt Man", "version": (1, 0), "blender": (3, 00, 0), "description": "Add Shaky motion to active object", "location": "Object Right Click -> Add Object Shake", "category": "Learning",
}
当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
sin((frame / fps) * length/9.8)))
任何命令行输入或输出都按以下方式编写:
'ASSETBROWSER_MT_context_menu',...['VIEW3D_MT_edit_metaball_context_menu', 'VIEW3D_MT_gpencil_edit_context_menu', 'VIEW3D_MT_object_context_menu', 'VIEW3D_MT_particle_context_menu',...
一些代码旨在作为交互式 Python 控制台输入使用。在这种情况下,用户输入以 >>> 提示符开头,与控制台输出不同:
>>> print("Hello")
Hello
粗体: 表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“在我们深入探讨如何编写 f-modifiers 脚本之前,我们将看看如何在图形编辑器中创建它们。”
小贴士或重要提示
它看起来像这样。
联系我们
我们读者的反馈总是受欢迎的。
一般反馈: 如果您对本书的任何方面有疑问,请通过电子邮件发送至 [customercare@packtpub.com,并在邮件主题中提及书名。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能向我们提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并附上材料的链接。
如果您有兴趣成为作者: 如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问 authors.packtpub.com。
分享您的想法
读完 Python Scripting in Blender 后,我们很乐意听到您的想法!请 点击此处直接进入此书的亚马逊评论页面 并分享您的反馈。
您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在路上阅读,但又无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
不要担心,现在每本 Packt 书籍都附带一本无 DRM 的 PDF 版本,无需额外费用。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不止于此,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的收件箱。
按照以下简单步骤获取福利:
- 扫描二维码或访问以下链接

https://packt.link/free-ebook/9781803234229
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱
第一部分:Python 简介
在这部分,我们将向您介绍 Blender 的 Python 接口。本部分将阐述脚本编写的基础知识,并提供如何为 Blender 编写工具的整体理解。
本节包括以下章节:
-
第一章,Python 与 Blender 的集成
-
第二章,Python 实体和 API
-
第三章,创建您的插件
-
第四章,探索对象变换
-
第五章,设计图形界面
第一章:Python 与 Blender 的集成
Blender 接受代码指令的方式与它交互的方式相同:通过图形用户界面。这允许艺术家和工作室实现他们自己的功能和自动化。
Python,一种高度可扩展且入门门槛低的编程语言,在计算机图形学中得到了广泛应用。
尽管 Blender 的核心是用 C 和 C++ 编写的,但菜单和图形元素使用 Python。这允许自定义和工厂功能具有相同的视觉和感觉。
在本章中,我们将学习如何在 Blender 中运行 Python 命令以及在哪里查找它们的输出。我们还将了解过去指令的历史记录在哪里,以及如何利用这一点。最后,我们将介绍代码编辑器和版本控制系统,并看看它们如何帮助我们简化工作。
到本章结束时,你将了解程序员是如何工作的,以及为什么这与最初使用软件并没有那么不同。
在本章中,我们将涵盖以下主要主题:
-
脚本工作区
-
Python 执行
-
版本控制
技术要求
除了 Blender 3.3+ 或在这种情况下,Blender 3.3 (www.blender.org/download/lts/3-3),你还需要以下免费工具:
-
Visual Studio Code,可在
code.visualstudio.com/Download下载。本书使用的是版本 1.66,可在code.visualstudio.com/updates/v1_66找到。 -
Git(可选),可在
git-scm.com/downloads找到。
鼓励你编写自己的代码;本章的示例可在以下 URL 找到:github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch1。
由于它是免费的开源软件,安装 Blender 有很多种方法。我们将探讨最常见的安装解决方案。
安装 Blender
安装 Blender 的过程因操作系统而异。像大多数应用程序一样,为 Microsoft Windows 和 Apple macOS 提供了安装程序。此外,适用于 Linux 和 Windows 的便携式版本,例如可以解压缩并在系统中的任何地方执行的存档,也是可用的。
本书使用的 Blender 版本 3.3 可能与系统上已安装的其他版本不同。在这种情况下,我们可以在同一台机器上安装不同版本的 Blender。
在 Windows 上安装 Blender
在 Microsoft Windows 上安装程序有多种方式:Windows 安装程序、Microsoft Store 和 使用便携式存档。虽然大多数应用程序发布者会选择其中之一,但所有这些选项都适用于 Blender。
通过 Windows 安装程序安装多个版本
在 Windows 上安装 Blender 最常见的方式是下载 .msi 文件来安装 Blender。在 Blender 3.4 之前,开始 菜单中只能使用多个安装版本中的一个。
如果您的情况是这样,其他版本可以通过导航到 Program Files 文件夹来访问,通常是 C:\Program Files\Blender Foundation,使用 Windows 资源管理器。
我们可以直接从 Blender 3.3 文件夹中执行 blender.exe,或者在 Windows 资源管理器中使用右键点击 > 新建 > 快捷方式来创建一个快捷方式:

图 1.1:程序文件中 Blender 的多个版本
或者,由于 3.3 是一个 长期支持 版本,它也可在 Microsoft Store 中找到。
从 Microsoft Store 安装 Blender
Blender 3.3 的下载页面提供了一个链接到 Microsoft Store 安装程序。或者,我们可以在顶部栏中启动 blender。一旦我们到达 Blender 页面,我们确保它是由 Blender Foundation 发布的,然后点击 获取 按钮:

图 1.2:从 Microsoft Store 安装 Blender
这将在 开始 菜单中添加一个 Blender 3.3 条目:

图 1.3:开始菜单中的多个版本
Microsoft Store 的优势是始终为安装的版本添加快捷方式。如果出于某种原因 Microsoft Store 不是一个选项,我们可以下载一个可移植的归档。
下载可移植归档
为了避免任何应用程序打包,我们可以下载一个 Windows 可移植的 .zip 文件,或者 Linux 的 .tar.xz 归档。
可移植版本可以从系统的任何位置使用,甚至是可移动驱动器。我们只需要通过右键点击 -> blender.exe 或解压文件夹中的 blender 可执行文件来在选择的目录中解压归档。
在 macOS 上安装多个版本
我们可以下载适用于 Apple Intel 或 Apple Silicon 计算机的 .dmg 包。下载后,双击文件将打开安装程序窗口。如果系统中已经存在另一个 Blender 版本,将弹出一个提示对话框,询问我们是否想保留两个版本或用新版本替换已安装的版本。
通过在文件管理器中应用程序条目上右键点击 -> 重命名 来选择 Blender 3.3:

图 1.4:在 macOS 上安装 Blender 的附加版本
安装 Blender 的方法有很多:下载页面提供了一个链接到如 Steam(Windows、macOS 或 Linux)或 Snapcraft(仅限 Linux)的商店,更不用说 Linux 发行版的包管理器(例如 Ubuntu 上的 apt、CentOS 上的 yum 等)。值得一提的是,可以从源代码构建 Blender,但这超出了本书的范围。
虽然这本书坚持使用版本 3.3,但示例应该可以在 3.x 系列的未来版本上运行,最多只需进行一些小的修正。
未来的一次重大发布,如 Blender 4 或 5,几乎可以保证与过去的脚本不兼容。尽管如此,本书关于最佳实践和思维模式的知识将经得起时间的考验。
现在我们已经在我们系统上安装了 Blender,我们可以深入探索其脚本功能。
脚本工作区 – 使用 Python 的第一步
一系列 Python 指令通常被称为 脚本。同样,生成 Python 代码的活动通常被称为 脚本编写。
Blender 的界面由不同的 工作区 组成。每个工作区都是一个标签页,用于不同的活动。在撰写本文时,脚本 标签位于屏幕右侧的最末尾。点击它将切换到为 Python 用户设计的界面。
最显著的部分是 Python 控制台、信息日志 和 文本编辑器:

图 1.5:Blender 脚本界面
我们将通过在 Python 控制台中输入命令来开始我们的 Python 之旅。
Python 控制台
控制台是一个交互式终端,其标题栏显示当前 Python 的版本(撰写本文时为 3.10.2)和一个 >>> 提示符,表明它正在等待交互式文本。我们只需将光标悬停在它上面并输入指令:

图 1.6:Python 控制台
“Hello World!”来自控制台
被称为 Hello World! 的实践是一种熟悉新编程语言的方式。它涉及到使用命令显示标题短语。
我们将使用 print 函数来做这件事。交互式控制台的示例代码以 >>> 提示符开始。我们不需要在控制台中输入那个,它已经在那里了。我们点击控制台区域并输入 print("Hello World"):
>>> print("Hello World")
然后按 Enter。控制台输出将以不同的颜色显示,并且不以提示符开头:

图 1.7:在控制台上显示我们的输出
我们可以使用 Python 控制台查询有关 Python 版本的信息。
检查 Python 版本
可以使用 sys 模块在任何时候显示当前 Python 的版本。我们需要导入这个模块并查找其 version 属性。这需要以下两行:
>>> import sys
>>> sys.version
控制台会打印出关于正在使用版本的详细信息:
'3.10.2 (main, Jan 27 2022, 08:34:43) ...'
版本号的三个数字代表 主版本、次版本 和 修订版本。不同的主版本号意味着语言语法的重大变化:Python 3.0 与任何 Python 2.x 版本有很大不同。次版本引入了新特性,但不会破坏与旧代码的兼容性。修订版本不会对语言进行更改;它仅包括错误修复和其他维护形式。
每个新 Python 版本带来的更改都可以在 Python 软件基金会 的下载页面上的 发布说明 中找到:
如果我们的脚本依赖于次版本引入的特性,我们可以使用 version_info 单独检查版本号,如下所示:
import sys
if sys.version_info.minor < 6:
print("Warning: This script requires Python 3.6.x")
与其他软件相比,Blender 非常紧密地遵循 Python 发布周期。这主要是为了利用性能和错误修复方面的最新改进。
检查 Blender 版本
当前 Blender 版本可以在图形用户界面或 Python 脚本中进行检查。
在界面中检查 Blender 版本
从 3.0 版本开始,检查 Blender 版本号最直接的地方是窗口的右下角。在 3.3 版本中,版本号后面跟着场景的当前时间和帧设置:

图 1.8:状态栏中的 Blender 版本号
另一种显示版本号的方法是点击菜单栏右上角的 Blender 图标,然后从菜单中选择 关于 Blender。
我们也可以通过 Python 脚本来获取 Blender 的版本号。
在 Python 脚本中检查 Blender 版本
如果我们的脚本依赖于特定版本的特性,它们必须能够确定正在运行的 Blender 版本。这些信息包含在 bpy.app 模块中。我们可以在控制台中输入以下行来显示当前版本:
>>> import bpy
>>> bpy.app.version
在 Blender 3.3.2 中,控制台返回以下信息:
(3, 3, 2)
与 sys.version_info 不同,bpy.app.version 不包含名称,只有数字。尽管如此,我们可以使用 Python 语法将它们存储在变量中:
>>> major, minor, micro = bpy.app.version
然后,我们可以使用 print 来显示单个版本号:
>>> print("Major version:", major)
Major version: 3
>>> print("Minor version:", minor)
Minor version: 3
>>> print("Micro version:", micro)
Micro version: 2
Blender 的新主版本会对界面和工作流程带来重大变化,而次版本会引入用于动画或生成图像的新工具。
为了显示这些信息,我们使用了 print 函数。由于函数是结构化编程的第一步,我们将更深入地了解它们的工作原理以及如何将 "Hello World!" 消息更改为其他内容。
调用函数
当我们使用一个 函数 时,我们说我们 调用 或 调用 那个函数。为此,我们输入其名称,然后跟随着括号。括号之间是函数的 参数,例如它操作的输入:

图 1.9:Python 脚本中的函数和参数
当调用时,print 函数读取参数并在新行上显示它。
"Hello World!" 参数是一个 字符串字面量:它可以是被引号 ("") 包围的任何字符序列。
我们可以将任何其他消息传递给 print;输出将相应地变化:

图 1.10:在 Blender Python 控制台中打印文本
现在我们已经获得了信心,我们将查看一些 Blender 命令。
信息日志
用户活动以 Python 命令的形式显示在日志区域,位于 脚本 工作区的左下角。我们可以打开 Blender 并执行以下操作:
-
通过右键单击 -> 删除 删除视口中的默认立方体。
-
从视口顶部栏,点击 添加 -> 网格 -> 圆柱体。
-
从视口顶部栏,点击 添加 -> 网格 -> UV 球体。我们将在信息日志区域找到这三行:

图 1.11:信息日志区域中的操作历史
信息日志的条目是我们最近活动触发的 Python 命令。我们可以复制这些行并将它们用于我们的脚本中。
使用日志中的行
使用左鼠标按钮单击或拖动选择日志行。我们可以通过右键单击 -> 复制 将它们复制到剪贴板:

图 1.12:从信息日志复制 Python 命令
我们可以回到启动场景并将它们粘贴到控制台:
-
重新启动 Blender 或点击 文件 -> 新建 -> 通用。
-
转到 脚本 工作区。
-
在 Python 控制台中,右键单击 -> 粘贴,然后按 Enter。
执行这些行将删除初始立方体,然后添加两个对象:之前手动运行的相同步骤。我们将看到如何更改它们的内容并影响结果。
更改参数
现在,我们暂时不要过于关注代码:它将在下一章中更加清晰。无论如何,我们可能会从 "Hello World!" 示例中识别出一个模式:
function(arguments between parentheses)
至少有一个参数在其目的上是一目了然的:
bpy.[…]_uv_sphere_add(…, …, location=(0, 0, 0), …)
location=(x, y, z) 表示添加新对象的三维坐标。我们可以更改最后一行,并在圆柱体上方创建我们的球体。
让我们再次回到启动场景,并将我们的行粘贴进去,但这次在我们按下 Enter 键之前,我们将最后一个零改为 2:
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 2), scale=(1, 1, 1))
我们刚刚运行了我们的第一个脚本。它删除了选定的对象,并在彼此之上堆叠了两个新形状:

图 1.13:通过 Python 创建的圆柱体和球体原语
Python 控制台可以立即执行代码,但不太适用于超过几行的代码。我们现在将看到如何将 Python 脚本作为文档运行。
文本编辑器
这是 脚本 工作区中最大的元素。它可以用来编写文本和脚本。
要添加新的脚本,我们点击顶部栏中的 + 新建(+ New)按钮并创建一个新的文本:

图 1.14:在文本编辑器中创建新的文本对象
让我们输入一些单词,例如,“Hello World!” 的更冗长的版本。像许多程序员编辑器一样,Blender 在左侧显示行号:

图 1.15:在文本编辑器中编写脚本
此外,单词根据其 Python 意义有不同的颜色:函数为白色,字符串为黄色,括号为红色。这个功能被称为 语法高亮,提供了有用的视觉反馈:单词的颜色取决于其在编程语言中的作用。
运行文本文档
如果当前文本是 Python 脚本,我们可以从文本编辑器中执行它:
-
在文本编辑器的菜单栏中,点击 运行脚本(Run Script)菜单下的 文本(Text)。
-
在信息日志中查找执行结果信息:

图 1.16:在文本编辑器中执行脚本
信息日志确认了有事情发生:

图 1.17:信息日志中提到的脚本执行
但我们可能会失望,因为打印出的文本似乎无处可寻!
原因是文本编辑器的输出直接进入 系统控制台,即 操作系统的 命令行。
在 Windows 上,我们可以通过从 Blender 的顶部栏选择 窗口(Window)->切换系统控制台 来显示它。要在基于 Unix 的系统(Linux 或 macOS)上读取系统消息,我们必须首先从命令行启动 Blender。
一旦打开,系统控制台 将显示文本编辑器打印的输出:

图 1.18:在 Windows 上显示系统控制台
我们文本块的默认名称是 Text。可以通过点击它来重命名。我们最好添加 .py 后缀作为扩展名,使其更清楚地表明它是一个 Python 脚本:

图 1.19:在 Blender 中重命名文本块
将 Python 控制台作为脚本复制
记得我们之前在 Python 控制台中输入的代码吗?如果我们没有关闭 Blender 或加载新的场景,我们可以立即将它们复制到 剪贴板。
-
从 Python 控制台顶部的 控制台 菜单中选择 复制 为脚本。
-
使用文本编辑器菜单中的 文本 > 新建 创建另一个文本块。
-
给文本起一个新的名字,例如
OurFirstScript.py。 -
通过在文本区域右键点击 -> 粘贴 将剪贴板中的行粘贴到文本区域。
看看文本编辑器,我们发现脚本的完整版本比我们的三行要长一些:

图 1.20:Python 控制台输入复制到文本编辑器
前五行设置控制台环境。它们在 Blender 启动时在幕后执行。
以哈希 (#) 开头的行是 注释:它们被 Python 忽略,包含为人类读者提供的提醒或解释。
我们自己的指令分别位于第 13、17 和 21 行。此脚本可以通过 文本 -> 运行脚本 如我们之前所做的那样执行,或者通过按 Alt + P 键组合。
导出文本文件
笔记本图标让我们可以通过下拉列表在不同的文本块之间切换:

图 1.21:在 Blender 之间切换文本块
从编辑器菜单栏中选择 文本 | 另存为… 将当前文本保存到磁盘。一个新窗口让我们选择文件夹并确认文件名:

图 1.22:将文本编辑器的内容保存到文件
Blender 的文本编辑器非常适合快速测试,但程序员文本编辑器通常更适合更严肃的任务。我们将在下一节中使用 Visual Studio Code。
外部编辑器 - Visual Studio Code
Visual Studio Code (VS Code)是微软的一个快速、多平台、免费的编辑器,适用于 Windows、macOS 和 Linux。使用外部编辑器可以使我们的代码独立于 Blender 的会话。此外,程序员文本编辑器除了语法高亮外,还提供了许多实用工具。
本书使用 VS Code 1.66。这是一个快速、轻量级的编辑器,适用于大多数平台,但有很多替代品 - 最值得注意的是以下这些:
-
Notepad++:这是一个快速但强大的 Windows 编辑器,可在
notepad-plus-plus.org.找到。 -
PyCharm:这是一个由 JetBrains 提供的 Python 集成开发环境 (IDE)。可以在
www.jetbrains.com/pycharm.找到免费的社区版本。 -
LightTable:这是一个有趣的开源编辑器,可在
lighttable.com.找到。 -
Sublime:这是一个商业文本编辑器,可在
www.sublimetext.com.找到。
大多数 Linux 发行版都至少包含一个不错的、现成的文本编辑器。我们鼓励您尝试并找到您喜欢的文本编辑器。
在本节中,我们将设置 VS Code 以进行 Python 脚本编写。
选择合适的录音室!
VS Code 和 Visual Studio 名称相似,但它们是微软的两个不同的产品。虽然 VS Code 是程序员文本编辑器,但 Visual Studio 是用于高级语言(如 C++)的完整开发环境。虽然 C++ 项目可能需要特定的构建环境版本,但只要 Python 是受支持的语言,就可以安全地使用任何版本的 VS Code。
加载我们的脚本文件夹
我们可以使用 .py 文件打开脚本文件夹来加载包含我们的脚本文件。
可以通过点击右下角的安装来安装额外的 Python 支持:

图 1.23:我们的 Python 脚本在 VS Code 中
保持 Blender 的文本块同步
当 Blender 中打开的文本文件被另一个应用程序更改时,文件名左侧会出现一个红色的问号:

图 1.24:Blender 检测已保存脚本中的更改
点击问号会显示可行的操作:
-
从磁盘重新加载:这将加载并显示最新的文件
-
使文本内部(单独的副本):显示的文本现在是 Blender 会话的一部分,不再与磁盘上的任何文本文件相关联
-
忽略:更改被忽略;Blender 仍会显示旧文本并继续报告它与磁盘上保存的文本不同步
为了获得额外的帮助,我们可以对我们的文件添加版本控制。这允许我们进行更改而不用担心破坏东西或丢失我们的工作。
版本控制和备份
版本控制有助于跟踪文件更改,保存代码的快照,并在必要时回滚到旧版本。Git是目前最常用的版本控制系统;它是免费的,并集成到大多数编辑器中。在本节中,我们将结合使用版本控制和 VS Code。
初始化存储库
安装 Git 后,可以通过 VS Code 使用它,通过在左侧列栏上的 分支 图标激活 源代码管理 选项卡。初始化存储库按钮将版本控制添加到我们的文件夹:

图 1.25:在 VS Code 中添加版本控制
图标将改变,并警告我们文件的存在。我们点击文件名旁边的 + 图标将它们添加到版本控制中。在 Git 术语中,我们将当前更改暂存:

图 1.26:在 VS Code 中暂存更改
编辑器显示了我们的文件的前/后条件。我们可以在左上角的文本框中添加一条消息并单击 勾选 图标。这将 提交 我们的变化到项目历史记录:

图 1.27:在 VS Code 中提交更改
进行更改
假设我们不想我们的脚本删除当前对象。为此,我们删除行号 13:
bpy.ops.object.delete(use_global=False)
当文件被保存时,版本控制会检测到这个更改。我们可以通过在左侧列中单击 OurFirstScript.py 来暂存它,VS Code 会突出显示当前更改。我们为这个新提交添加一条消息并再次单击 勾选 按钮:

图 1.28:在 VS Code 中显示更改
如果我们回到 资源管理器 选项卡并选择我们的脚本,我们将看到有一个名为 时序线 的部分可以被展开:它包含我们的提交消息列表。选择一个提交将显示相关更改,允许我们恢复旧代码行。每个未提交的更改都可以通过使用 撤销 功能轻松撤销。
撤销未提交的更改
让我们在行 7 添加一些错误文本并保存。如果由于任何原因我们无法撤销,我们可以在 版本控制 选项卡中右键单击我们的文件并选择 丢弃更改:

图 1.29:在 VS Code 中丢弃未提交的更改
版本控制的重要性一开始可能被低估,但在更复杂的项目中变得至关重要。这是一个广泛的话题,超出了本书的范围,但至少掌握其基础知识是很重要的。
摘要
在本章中,我们通过 脚本编写 增强了信心,并介绍了 Python 编程的基本工具。你学习了如何在 Blender 日志中查找 Python 命令并在不同上下文中执行它们,以及如何设置编码环境。我们还学习了如何跟踪我们的代码和编辑。
在 第二章 中,我们将深化我们对 Python 的知识。我们将遇到最常见的实体,并学习如何使用编程逻辑编写更有用的脚本。
问题
-
我们如何在 Blender 中显示接受 Python 输入的部分?
-
我们如何读取 Python 执行的输出和打印输出?
-
Blender 是否使用 Python 来执行用户操作?
-
我们如何查看 Blender 的 Python 活动日志?
-
我们如何在 Blender 中编写脚本?我们能否在其他应用程序中编辑它们?
-
我们如何在哪个选项卡中初始化 VS Code 中的版本控制?
-
我们如何在 VS Code 中访问脚本的时序线?
第二章:Python 实体和 API
Blender 通过使其应用程序编程接口(API)的模块在应用程序内部可用来扩展 Python。
这些模块提供了包装器,将 Blender 的内部数据转换为 Python 对象。完整的文档和 API 参考可在网上找到,并可以从应用程序内部访问。此外,还有一些额外的功能可以帮助程序员在他们的旅程中。
就像我们在第一章中遇到的语法高亮一样,一些针对开发者的特性在编程世界中很常见。其他特性,如属性工具提示和变量显示,是 Blender 特有的。
在本章中,我们将查看一些代码片段,即代码块,这将帮助您对 Blender 的 API 架构有信心。
通常,API 的设计对已经熟悉 Python 的程序员非常友好,仅在少数情况下偏离标准。
到本章结束时,您将能够从 Python 控制台检查 Blender 对象并更改其属性,使用和扩展 Blender 收藏夹,并检查用户交互的当前状态。
在本章中,我们将熟悉以下主题:
-
Python 的特性
-
Blender 模块及其结构
-
数据和上下文访问
技术要求
只需要 Blender 就可以跟随本章的内容。
Python 的有用特性
我们在第一章中已经遇到了脚本工作区的Python元素。现在我们将探讨一些有用的特性,这些特性可以帮助我们充分利用它们。在编程方面,自动化可以加快搜索属性和术语的速度。这可以在控制台通过传统方法如自动完成实现,或者在界面中通过显示图形元素 Python 地址的快捷键实现。其中一些功能在 Blender 启动时就已经可用,而其他功能则留给用户启用。
开发者选项
开发者功能默认禁用。它们可以在 Blender 顶部栏的编辑菜单中的首选项对话框中启用。我们需要选择左侧的界面选项卡并查看第一个面板:显示。程序员通常启用开发者额外功能和Python 工具提示选项。
开发者额外功能
开发者额外功能添加了一个右键菜单项,可以在文本编辑器中显示 UI 的 Python 源代码。当用户切换到编辑模式时,它还会显示网格组件的几何索引。此外,它还允许搜索栏执行无法通过 UI 访问的操作员。
Python 工具提示
将鼠标光标悬停在属性上会显示一个包含简要描述的工具提示。如果启用了 Python 工具提示,还会显示如何在脚本中调用该属性的信息。

图 2.1:Blender 的显示首选项
例如,在 3D 视图中,我们可以按N键来显示屏幕右侧的变换通道。将鼠标指针停留在坐标上,例如位置: X,一段时间将显示描述中的两条附加行:

图 2.2:对象位置的 Python 提示
Python 提示包含两行:
Python: Object.location
bpy.data.objects['Cube'].location[0]
第一行提供了属性的 Python 名称;在这种情况下,location是空间中的Object位置。
第二行更具体:可以通过在控制台中输入该行来访问此对象('Cube')的位置。这通常被称为属性的完整数据路径,在某些情况下,也称为RNA路径。后者来自一个有趣的类比于遗传学:如果 Blender 的内部代码是其DNA,那么其Python访问可以看作是应用的RNA。
一个对象的定位是一个简单的情况,其他属性可能更复杂。无论如何,我们可以按照下一小节中的步骤将数据路径复制到剪贴板。
复制数据路径
在属性上右键单击将打开一个上下文菜单。一些条目,例如插入关键帧和重置为默认值,对动画很有用。在本节中,我们将重点关注编程条目,复制数据路径和复制完整 数据路径:
-
在 3D 视图中选择一个对象。
-
如果右侧没有显示变换属性,请按N键召唤变换侧边栏。
-
从项目选项卡,右键单击第一个位置通道(X),然后点击复制完整 数据路径。

图 2.3:位置 X 的右键菜单
- 前往 Python 控制台,按Ctrl + V粘贴,然后按Enter。
控制台将显示X坐标的位置值:
>>> bpy.data.objects['Cube'].location[0]
0.0
完整数据路径允许访问一个属性,除非它是只读属性,否则可以更改其值。我们可以看到复制的行以一个索引结束,这是由于location是一个三维属性——每个索引都指空间的一个轴:
bpy.data.objects['Cube'].location[0] # X axis
bpy.data.objects['Cube'].location[1] # Y axis
bpy.data.objects['Cube'].location[2] # Z axis
数据路径有时可能很复杂,但在下一节中,我们将查看一些控制台工具,这些工具在寻找正确的属性时非常有帮助。
Python 控制台的实用工具
Python 控制台提供了一些有用的工具。其中一些,如文本补全和历史记录,在程序员工具中很常见。其他一些,如变量的 3D 表示,是 Blender 特有的。本节概述了 Python 控制台在日常编程中的应用。
自动补全
在控制台中输入时按下Tab按钮会建议几种完成该行的可能方式。除此之外,如果当前语句与内部文档相关联(例如print():
-
在 Blender 中,在屏幕顶部的选项卡中选择脚本工作区,正如我们在脚本工作区部分、Python 的第一步、第一章中学到的。
-
在 Python 控制台中,只输入
prin,然后按Tab。
控制台填写缺失的字母,并显示print(,一个开括号及其文档。然后,它让程序员完成该行。

图 2.4:Python 控制台中的自动完成
历史
可以使用上下箭头键检索 Blender Python 控制台中之前执行的命令。这可以通过任何代码进行测试。以下是在 Python 控制台中运行的示例:
-
输入
print('Line One'),然后按Enter。 -
按下↑键。当前文本将更改为以下内容:
>>> print("Line One") -
删除最后几个字母,将行更改为
print('Line Two'),然后按Enter。 -
按两次↑键以再次显示
>>> print('LineOne')。 -
在↓和↑之间交替按动以在两个命令之间切换。
多行输入
由两行或更多行组成的代码片段可以粘贴到控制台,并通过按两次Enter键执行。
由于空白行标记了代码片段的结束,它们可能会使存在于缩进块内的有效代码失败。让我们看看一个简单的例子:一个包含两个print语句的条件,这两个语句由一行分隔:
if True:
print('Line One')
print('Line Two')
此代码在文本编辑器中有效,但在Python 控制台中失败。以下是输出:
>>> if True:
... print('Line One')
...
Line One
>>> print('Line Two')
File "<blender_console>", line 1
print("Line Two")
IndentationError: unexpected indent
执行前两行后,第二行print()的缩进被认为是错误的。
每个针对控制台设计的代码片段中的空白行都应该用注释(#)替换。以下代码将有效:
if True:
print('Line One')
#
print('Line Two')
在 3D 视图中显示 3D 变量
表示 3D 点或变换的变量可以在三维空间中显示。这是通过Math Vis (Console)插件实现的。插件是 Python 扩展,可以在需要时启用。本书中我们将编写自己的插件。目前,我们将看看如何启用随 Blender 一起提供的插件。
启用 Math Vis (Console)插件
插件可以在首选项中启用:
-
从顶栏菜单中选择编辑 | 首选项。
-
在左侧列中选择插件选项卡。
-
在带有放大镜图标标记的搜索过滤器中输入
Math Vis。 -
点击插件名称左侧的复选框。
创建 3D 变量
Blender 为 3D 实体提供了额外的 Python 类型。例如,可以使用Vector类型存储坐标。我们可以通过输入以下内容将向量存储在变量中:
my_vector = Vector([1.0, 2.0, 3.0])
由于我们已经启用了Math Vis (Console)插件,3D 视图中将出现一个粉红色的点,后面跟着变量名。
![图 2.5:向量坐标 [1.0, 2.0, 3.0],如图 3D 视口所示](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-scp-bld-3x/img/Figure_2._05_B18375.jpg)
图 2.5:向量坐标 [1.0, 2.0, 3.0],如图 3D 视口所示
变量只有在控制台可见时才会可视化,只要它们存在。一旦它们被删除,绘图就会停止,如下所示:
del my_vector
del 语句是标准的 Python 命令。我们应该记住,它删除的是 Python 变量,而不是 Blender 对象。
如果我们想在 Blender 中删除对象,我们可以使用 Blender 的 delete() 命令:
bpy.ops.object.delete()
前一个命令中的 ops 代表 bpy.ops.object.delete(),这意味着按 X 键或从 对象 菜单中选择 删除 操作。
具有几何意义的类型,如 Vector、Matrix 和 Euler,是数学构造,属于 mathutils 模块。此模块会自动导入到 控制台 中。在控制台中不需要再次导入它。如果我们想在脚本中使用它,我们必须从模块中导入:
from mathutils import Vector
my_vector = Vector([1.0, 2.0, 3.0])
我们将在下一章中探索 mathutils,当处理 3D 对象和元素时。在下一节中,我们将熟悉 Blender 对象如何转换为 Python。
访问 Blender 模块
Blender 的附加模块在整个应用程序中可用,可以通过标准 import 语句使用。它们在 Python 控制台、文本编辑器和通常在 Blender 系统和用户路径中安装的脚本中可用。
一些模块非常特定;例如,freestyle 模块处理自由风格渲染的设置,不能用于其他目的。其他模块,如 mathutils,在涉及数字时都会发挥作用。
最后,bpy 模块及其子模块在 Blender 脚本中扮演着更重要的角色,因为它们提供了访问对象和数据的能力。
在本节中,我们将更详细地了解 bpy,它在控制台中已经存在,以及我们如何在脚本中使用它。我们还将学习如何找到有关 API 及其元素更多信息。
bpy 模块
在 第一章 中,我们使用 Python 控制台编辑器的 控制台->复制 从控制台复制了行,并将它们粘贴到一个文本块中。在这个过程中,我们在开头发现了一些额外的行:
import bpy
from bpy import data as D
from bpy import context as C
...
第一行导入 bpy,这是编程接口的主模块。第二行和第三行导入 data 和 context,并将它们分别分配给 D 和 C 字母作为便利快捷方式。这最初在屏幕上指出:

图 2.6:Python 控制台的便利变量
data 表示 Blender 对象的存储,而 context 则是用户交互的当前状态,例如选择或当前模式(对象、编辑、姿态等)。
由于其本质,context 和 data 总是存在于 Blender 脚本中。自动补全显示了其他模块的概览。如果我们输入 bpy. 并按 Tab 键,我们将获得它们的列表。

图 2.7:bpy 的子模块
bpy 的每个属性都涵盖了 Blender 的一个特定方面。例如,bpy.app 包含软件的属性(可执行文件和版本),而 bpy.ops 包含操作员,即可以在界面中调用的函数。
bpy 和其他 Blender 模块包含大量的类、方法和实用工具。这些实体在 Python API 参考文档 中进行了文档化,该文档可在网上找到,如有需要可以下载。
API 文档
可以通过 帮助 | Python API 参考文档 从顶部菜单栏访问参考网站。

图 2.8:Python API 参考链接
当前版本的 Python 帮助 将在网页浏览器中打开。该文档是通过使用名为 help() 函数的软件从 docstrings 生成的。

图 2.9:help() 函数与 Euler 类在线帮助的比较
在线帮助具有搜索栏的优势,并且不会占用我们的 Blender 会话空间。它包含可用模块及其内容的索引。
API 参考非常有助于导航各种模块和 bpy 的属性。
在本章中,我们将重点关注 bpy.data 和 bpy.context,将其他模块的具体功能留到下一章介绍。
访问 Blender 数据
当前会话中创建的所有实体都作为 bpy.data 的一部分可用。它们被分组在遵循 bpy.data.armatures、bpy.data.curves 等的类别中。每个类别都是一个 bpy_collection,它是 Blender 类型,包含更多元素。它们的内容可以通过索引(如 Python list 中的索引)或通过关键字(如字典中的关键字)访问。
对象访问
我们可以使用 Python 访问场景中的对象。例如,我们可以查询 Blender 默认场景的内容,该场景包含一个 立方体、一个 相机 和一个 灯光:
- 打开或重启 Blender,然后在屏幕顶部的标签页中选择 脚本工作区。

图 2.10:工作区标签页
-
输入
len(bpy.data.objects)并按 Enter 键:>>> len(bpy.data.objects)3 -
在 Python 控制台中,输入
bpy.data.objects,然后按 Tab 键。

图 2.11:Blender 的默认对象
初始时可能会感到困惑,因为不同类型的对象都属于 bpy.data.objects,而不是 bpy.data.cameras、bpy.data.meshes 和 bpy.data.lights。
实际上,在 3D 视图中可以放置和显示的任何内容都是bpy.data.objects类型。对象是一个通用的容器,可以存储任何类型的数据,或数据块。对象/数据块系统是 Blender 的一个原则。我们将在下一章中更好地了解它。现在,我们将专注于对象级别的访问。
列表式访问
与 Python 列表一样,bpy_collection的各个元素可以通过在括号中添加索引数字来访问,如下例所示:
>>> bpy.data.objects[0]
bpy.data.objects['Camera']
>>> bpy.data.objects[1]
bpy.data.objects['Cube']
>>> bpy.data.objects[2]
bpy.data.objects['Light']
如果我们知道我们正在寻找的对象的名称,我们可以通过字符串关键字而不是索引来获取它。
字典式访问
除了使用它们的序号索引外,我们还可以使用它们的名称作为关键字来访问bpy.data.objects的元素,就像我们在 Python 字典中做的那样:
>>> bpy.data.objects['Camera']
bpy.data.objects['Camera']
>>> bpy.data.objects['Cube']
bpy.data.objects['Cube']
>>> bpy.data.objects['Light']
bpy.data.objects['Light']
遍历集合
要对一个聚合类型中的所有对象执行表达式,我们需要遍历这个集合。遍历描述了滚动元素的动作。通过使用循环语句遍历,我们可以在许多对象上执行相同的操作。
列表式循环
典型的for element in list循环与bpy_collection一起工作。以下片段打印出现有对象列表:
import bpy
for ob in bpy.data.objects:
print(ob.name, ob.type)
或者,如果我们还需要它们的集合索引,我们可以使用以下方法:
import bpy
for i, ob in enumerate(bpy.data.objects):
print(i, ob.name, ob.type)
查看输出结果,我们可以看到bpy.data.objects的元素是按字母顺序排序的:
0 Camera CAMERA
1 Cube MESH
2 Light LIGHT
这意味着重命名对象会改变它们在列表中的顺序。如果我们迭代集合时重命名元素,这可能会成为一个问题。
例如,这个片段在第一个对象(相机)的名称前添加了字母'z'。这改变了它在大纲视图中的位置,从第一个显示的对象变为最后一个显示的对象:
import bpy
bpy.data.objects[0].name ='z' + bpy.data.objects[0].name

图 2.12:重命名前后的对比 – 对象的顺序已改变
如果我们在循环内部执行相同的操作,我们将遇到重新排序的问题:
import bpy
for ob in bpy.data.objects:
ob.name ='z' + ob.name
如果'z'重复一段很长时间,这是合理的。

图 2.13:重命名添加了过多的“z”
这是一个错误,我们的代码和应用程序本身并没有做任何本质上的错误。
要理解为什么会这样,我们需要将其分解成单个步骤。Blender 重命名了第一个、第二个和第三个对象,然后它应该停止。但由于它们已经被重命名,"Light"被重命名并放在最后,所以之后,Blender 继续将"zCamera"重命名为"zzCamera",这个过程一直持续下去。
这会持续到名称变得过长而无法重命名为止。
这种类型的错误可能导致软件停止运行,并且很难找到。每次我们的脚本重命名集合的内容时,我们必须确保重新排序不会成为问题。我们将探讨一些可能的解决方案。
通过列表转换避免重新排序
避免重复迭代的第一个也是最简单的方法是将bpy_collection转换为 Python 列表。按Ctrl + Z撤销重命名。
现在,我们将使用一条略微不同的命令,通过list()方法将集合转换为纯 Python 列表:
import bpy
for ob in list(bpy.data.objects):
ob.name = 'z' + ob.name

图 2.14:对象已被正确重命名
在下一小节中,我们将看到字典方法也得到了支持。它们在许多方面都是不可重新排序的。
类似字典的循环
就像 Python 字典一样,keys()方法返回集合中所有存在的名称:
for name in bpy.data.objects.keys():
print(name)
或者,我们可以使用values()方法获取对象的列表:
for ob in bpy.data.objects.values():
print(ob.name, ob.type)
最后,我们可以使用items()迭代:
for name, ob in bpy.data.objects.items():
print(name, ob.type)
创建新对象
Blender 类故意缺少bpy_collection的new()方法。例如,3D 对象是通过bpy.data.objects.new()创建的。
new()方法
使用 Blender 界面将对象添加到场景中只需一步。在 Python 中完成此操作需要一些额外的工作:new()命令将新对象存储在内存中,但然后我们需要显式地将它添加到我们的场景中。
在 Python 控制台中输入bpy.data.objects.new并按Tab键将显示其文档:
>>> bpy.data.objects.new(
new()
BlendDataObjects.new(name, object_data)
Add a new object to the main database
新函数需要两个参数:我们想要给对象起的名字以及它将要包含的datablock。如果我们还没有任何datablocks,我们可以为object_data参数创建一个None类型:
import bpy
my_empty = bpy.data.objects.new('My Empty', None)
print('New Empty created:', my_empty)
print()行将确认对象已被创建。它不会在 3D 视图中显示,但我们可以检查bpy.data.objects。

图 2.15:新的空对象出现在 Python 集合中
此对象还不是 3D 场景的一部分。为了成为场景的一部分,对象必须属于一个集合。
对象集合
术语集合有些含糊不清,因为我们把bpy_collection称为数据访问的一部分。bpy_collection类型,如bpy.data.objects,是两个不同的概念:
-
场景集合是作为文件夹显示在大纲视图中的对象组。它们用于在场景中组织 3D 对象。
-
bpy_collection不是场景的一部分。
所有场景集合都可以使用bpy.data.collections在 Python 中访问。
我们现在只需一步就可以将我们的对象添加到场景中:我们需要将my_empty添加到场景集合中,使用集合方法link。
将对象链接到场景
默认场景中只有一个集合,因此如果我们输入bpy.data.collections并按Tab键,我们可以通过自动完成获取它:
>>> bpy.data.collections['Collection']
默认集合命名为Collection.objects属性。将my_empty链接的 Python 行是:
bpy.data.collections['Collection'].objects.link(my_empty)
我的空对象现在已成为场景的一部分,并在大纲视图中显示出来。

图 2.16:我们的空对象与其他对象一起
删除元素
就像我们可以使用 new() 创建新元素一样,我们可以使用 bpy.data.objects 的 remove() 方法来删除它们。这一行将 my_empty 从 Blender 中移除:
bpy.data.objects.remove(my_empty)
通过 link() 链接现有对象的集合,如 Collection.objects,有一个 unlink() 方法用于删除:
collection = bpy.data.collections['Collection']
collection.objects.unlink(bpy.data.objects['Cube'])
在那种情况下,bpy.data.objects。
在本节中,我们通过 Python 使用 bpy.data 访问了 Blender 对象。
如果有多个场景,或者当前对象和活动选择,我们如何获取当前场景?
我们将看到如何在 bpy.context 模块中跟踪用户交互的状态。
理解用户上下文
当前交互状态、当前场景和选择都可以通过 bpy.context 获取。由于它依赖于用户操作,bpy.context 是只读的;也就是说,不能直接更改。无论如何,我们可以通过 Python 影响当前活动的状态。而不是更改 bpy.context 的属性,我们必须查找 Blender 对象、图层和场景的选择和活动属性。
活动场景
一个 .blend 文件,或者说是未保存的会话,可以包含多个场景。这与 3D 软件包的标准不同,其中保存的文件相当于一个场景。如果有更多场景可用,它们可以从 Blender 头部右上角的列表菜单中选择。
每个场景都可以包含 bpy.data.objects 中的任何对象,一个对象可以属于多个场景。对一个场景中的对象所做的更改将保留在其他场景中。
我们已经看到了如何使用 bpy.data.objects.new() 创建新对象。我们可以用相同的方式创建新场景,使用 bpy.data.scenes.new():
import bpy
new_scene = bpy.data.scenes.new('My Scene')
print('New scene created:', new_scene.name)
新场景将在右上角的控件中可用。

图 2.17:Blender 场景菜单
当前显示的场景包含在 bpy.context.window 中。
如果我们想创建一个新场景并使其成为活动场景,我们可以将其分配给 window.scene 属性:
import bpy
new_scene = bpy.data.scenes.new('Another Scene')
bpy.context.window.scene = new_scene
执行此代码片段后,3D 视口将切换到一个新的空场景。当前场景本身是 bpy.context 的一部分,可以通过 Python 使用 bpy.context.scene 来检索:
print('The current scene is', bpy.context.scene.name)
视图层
视图层用于分别渲染场景中的对象,并通过合成将它们重新组合。这样做是为了加快渲染过程,例如,只渲染一次背景,或者出于艺术需求。在 UI 中,视图层与场景以相同的方式创建,使用顶部栏上的控件。
在 Python 中创建它们时,我们必须记住它们始终属于它们的场景,而不是 bpy.data。尽管如此,如果我们想设置活动图层,我们仍然必须使用 bpy.context.window 的一个属性:
import bpy
new_layer = bpy.context.scene.view_layers.new('My Layer')
print('New layer created:', new_layer.name)
bpy.context.window.view_layer = new_layer
print('Current layer:', bpy.context.view_layer.name)
活动层必须属于活动场景。尝试将来自不同场景的层分配给当前窗口的语句将被忽略。以下是一个示例:
import bpy
new_layer = bpy.context.scene.view_layers.new('Another Layer')
print('New layer created:', new_layer.name)
new_scene = bpy.data.scenes.new('Another Scene')
bpy.context.window.scene = new_scene
# NOTE: the following line will not work
bpy.context.window.view_layer = new_layer
print('Current layer:', bpy.context.view_layer.name)
图层可以存储渲染和通道属性,但也可以存储它们的可见性、活动状态和选择状态。在下一节中,我们将看到图层是如何存储活动对象的。
活动对象
当用户选择一个对象时,它将成为当前层的活动对象。其属性将在界面中显示,并且将成为用户操作的主要目标。
当 Blender 打开时,默认的活动对象是一个立方体。我们可以从左上角的Text Info中看到这一点。

图 2.18:Blender 的默认活动对象
在 API 中可以在多个地方检索活动对象,最直接的是bpy.context.object:
bpy.context.object # read only
bpy.context.active_object # read only, same as above
bpy.context.view_layer.objects.active # can be set
所有三个属性都指向同一个对象,但由于bpy.context是只读的,因此只能通过程序更改view_layer属性。如果有更多图层,切换图层可以更改活动对象。或者我们可以通过以下步骤使用 Python 来更改它。
更改活动对象
活动对象是活动视图层的属性。考虑到这一点,我们只需将view_layer.active属性设置为不同的对象即可。例如,以下是如何选择相机的示例:
-
打开 Blender 或通过文件->新建->通用恢复到默认场景。
-
前往脚本工作区。
-
在 Python 控制台中输入以下行并按Enter键:
import bpy
view_layer = bpy.context.view_layer
view_layer.objects.active = bpy.data.objects['Camera']
我们可以看到,活动对象已经从 3D 视图、属性和状态信息中改变。

图 2.19:相机现在是活动对象
我们还可以看到立方体仍然被选中,而相机尽管是活动对象,却没有被选中。这是因为活动状态和选中状态是两个不同的概念。在下一节中,我们将看到它们之间的区别,以及如何查询和更改当前选择。
保持专注
Blender 的焦点策略一开始可能会让人困惑。鼠标光标下的区域接收键盘输入。
对于艺术家来说,这并不是一个大问题,因为他们的任务通常涉及保持光标在操作区域。但对于程序员来说则不同;我们可能会认为只为控制台输入代码行,却发现我们在 3D 视图中触发了快捷键,或者反过来。
已选对象
通过在 3D 视图中按A键(从菜单栏选择Select | All)来选择场景中的所有对象。然后,在控制台中,我们输入以下内容:
>>> bpy.context.selected_objects
[bpy.data.objects['Cube'], bpy.data.objects['Light'], bpy.data.objects['Camera']]
selected_objects是一个 Python 列表。与bpy.data.objects不同,它按创建时间排序,而不是按字母顺序排序。对象从不按选择时间排序;Blender 根本不保留这些信息。如果我们的工具需要以特定顺序选择对象,我们必须在其他地方存储顺序。
选择一个对象通常使其成为当前层的活动对象。我们可以使用与bpy.context.object的比较来打印出哪个对象是活动的:
import bpy
for ob in bpy.context.selected_objects:
if ob is bpy.context.object:
print(ob.name, 'is active, skipping')
continue
print(ob.name, 'is selected')
在所有默认对象被选中的情况下运行此代码片段将产生以下输出:
Cube is active, skipping
Light is selected
Camera is selected
当我们想要从活动对象传播属性到选择集时,这种模式非常有用。我们已经看到,我们不应该假设活动对象总是被选中。反转选择将取消选中活动对象,但它将保持活动状态。或者可能根本就没有活动对象;它可以通过以下行删除或设置为None:
bpy.context.view_layer.objects.active = None
使用in运算符可以检查活动对象是否属于选择集:
is_sel = bpy.context.object in bpy.context.selected_objects
更好的是,我们可以使用select_get()和select_set()对象属性:
is_sel = bpy.context.object.select_get()
与活动对象一样,每个对象的选择状态都是按视图层存储的。在下一节中,我们将学习如何影响当前选择以及它的存储位置。
更改选中的对象
使用object.select_get()和object.select_set()查询和设置对象的选择状态,因为select_set函数的工作方式,我们可以在 Python 控制台中开始输入它,直到括号处:
>>> bpy.context.object.select_set(
按下Tab键将显示函数及其参数的描述:
select_set()
Object.select_set(state, view_layer=None)
Select or deselect the object. The selection state is per view layer
如果没有指定视图层,则使用当前层的选择集。例如,此代码片段将取消选择当前层中的所有对象:
import bpy
for ob in bpy.context.selected_objects:
ob.select_set(False)
我们可以动态创建层并将不同的选择分配给它们。以下是一个创建视图层并选择网格对象和另一个选择相机的代码片段:
import bpy
m_layer = bpy.context.scene.view_layers.new('Sel_Mesh')
c_layer = bpy.context.scene.view_layers.new('Sel_Cam')
for ob in bpy.data.objects:
ob.select_set(ob.type == 'MESH', view_layer=m_layer)
ob.select_set(ob.type == 'CAMERA', view_layer=c_layer)
选择是用户从场景中挑选对象的最直接方式。因此,bpy.context在脚本中起着关键作用,通常可用,甚至作为 Python 对象传递。
摘要
在本章中,我们看到了 Python 如何通过bpy.data访问 Blender 的内容,并介绍了空间实体,如向量。我们还看到了如何通过bpy.context与用户活动交互,以及如何通过影响对象和层的状态来改变上下文的只读属性。
在第三章中,我们将看到如何将我们的例程插入到自己的插件中,并使其准备好在 Blender 中安装和使用。
问题
-
哪些辅助实用工具是 Blender 典型的?
-
我们如何存储和显示空间坐标?
-
bpy的哪个属性可以访问所有 Blender 实体? -
Blender 对象的 Python 类有构造函数吗?
-
我们如何创建新的 Blender 对象?
-
对象活动意味着什么?
-
Blender 场景中的活动对象是一个属性吗?
-
我们能否使用
bpy.context来影响选择? -
我们能否使用
bpy.context.view_layer来影响选择?
第三章:创建您的扩展插件
扩展插件是扩展 Blender 功能的插件,可以在首选项中启用。其中一些,如在第2 章中遇到的 Math Vis,是作为可选功能分发的官方功能。其他的是第三方扩展,用户可以安装。
在本质上,扩展插件是包含 Blender 安装、启用和删除所需信息的 Python 模块,就像插件系统一样。
在本章中,你将学习如何在 Blender 中编写和安装扩展插件,以及如何在制作过程中启用扩展插件。我们还将实现一个新的命令,将对象分组到集合中,并将其作为对象上下文菜单的一部分。
本章将涵盖以下主题:
-
编写 Blender 扩展插件脚本
-
运行和更新我们的扩展插件
-
修复错误和改进我们的代码
技术需求
我们将使用 Blender 和Visual Studio Code(VS Code)。本章创建的示例可以在github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch3找到。
在 Blender 中安装我们的扩展插件
我们可以使用 VS Code 编写一个非常简单的扩展插件。这个扩展插件实际上并没有做什么;它只是在扩展列表中显示。
首先,我们必须为本章的代码创建一个文件夹。我们可以使用文件管理器或大多数 IDE 附带的重定向侧边栏。在这个例子中,我们将使用我们在第一章的外部编辑器部分遇到的 VS Code:
-
在 VS Code 中打开您的PythonScriptingBlender项目。
-
点击新建文件夹图标创建一个新文件夹。

图 3.1:在 Visual Studio Code 中创建文件夹
- 将新文件夹命名为
ch3。
现在,我们可以为我们的扩展插件创建一个 Python 文件:
- 确保在VS Code资源管理器中选择
ch3文件夹,然后点击新建文件图标创建一个新文件。

图 3.2:在 VS Code 中创建文件
-
将新文件命名为
the_simplest_add_on.py。 -
双击打开文件。
我们准备好编写我们的扩展插件了;让我们看看需要什么。
扩展插件需求
要被视为扩展插件,我们的代码必须包含三样东西:
-
脚本元信息——即有关扩展插件的信息
-
一个用于启用扩展插件的
register()函数 -
一个用于禁用扩展插件的
unregister()函数
脚本元信息
首选项标签页中显示的信息来自bl_info变量,这是一个位于.py文件顶部的字典。该字典必须包含作者的姓名、扩展插件的简短描述以及为其编写的 Blender 版本。以下是我们的简单扩展插件的详细信息:
bl_info = {
"name": "The Simplest Add-on",
"author": "John Doe",
"version": (1, 0),
"blender": (3, 00, 0),
"description": "A very simple add-on",
"warning": "This is just for Learning",
"category": "Learning",
}
从空白开始!
在我们的代码开始和结束处留一个空行会更好——不以空行开始的.py文件可能无法注册为附加组件并导致missing bl_info错误。
注册
当附加组件启用时,会执行register()函数。目前并没有什么动作发生——只有一个pass语句,因为我们的函数没有做任何事情:
def register():
# this function is called when the add-on is enabled
pass
当附加组件禁用时,会调用unregister()函数。与register()类似,它目前还没有做什么,但它是一个附加组件所必需的。
def unregister():
# this function is called when the add-on is disabled
pass
安装
现在,是时候在 Blender 中安装我们的附加组件了:
-
通过顶部菜单中的编辑 | 首选项打开首选项窗口。
-
在左侧列中选择附加组件选项卡。
-
在附加组件首选项的右上角点击安装按钮。
-
在文件浏览器中导航到
PythonScriptingBlender\ch3并选择the_simplest_add_on.py。 -
在底部点击安装附加组件按钮。
我们的附加组件已经被复制并安装到 Blender 中;左上角的过滤器输入被填充,以便只显示新的附加组件。我们可以点击附加组件复选框来启用它。展开展开三角形可以显示更多来自bl_info的信息。

图 3.3:Blender 中列出的一个非常简单的附加组件
我们字典中的warning条目以三角形图标显示。这一行是为了警告用户可能不稳定的代码。
现在我们已经完成了附加组件的功能,是时候移除它了。
卸载
在附加组件首选项中点击大的移除按钮将显示一个确认对话框,询问是否可以删除附加组件。这个操作不能撤销,但在这个情况下,继续移除最简单的附加组件是可以的:

图 3.4:Blender 中附加组件的移除
移除对话框中显示的路径表明附加组件被安装在了 Blender 用户首选项中。这并不总是情况,正如我们将在下一段中看到如何将脚本路径指向我们的工作目录。
脚本路径
在开发过程中每次更改时重新安装附加组件将变得不切实际。程序员通常会为 Python 脚本设置一个系统路径并在那里工作附加组件。
系统路径可以在Blender 首选项中找到,通过在左侧列中选择文件路径选项卡。

图 3.5:文件路径首选项窗口
我们可以将这个路径设置为我们将用于脚本处理的目录,例如托管本章代码的PythonScriptingBlender/ch3文件夹。
附加组件文件夹
现在 Blender 将查看我们的脚本文件夹,我们可以为我们的附加组件创建一个目录。我们可以从 VS Code 中这样做:
-
在 VS Code 中选择
PythonScriptingBlender/ch3。 -
通过点击新建 文件夹图标创建一个新的文件夹。
-
将新文件夹命名为
addons。
重要的是addons是这个文件夹的确切名称;否则,Blender 将不会寻找扩展。我们需要重新启动 Blender 以使文件路径设置生效,但一旦我们这样做,Blender 将能够加载我们正在工作的插件,无需安装。
现在,我们可以开始工作于一个新的插件,该插件为 Blender 添加功能。在下一节中,我们将编写一个插件,将场景中的对象分组到集合中。
创建我们的第一个插件 – 对象收集器
我们将编写一个插件,将场景中的对象分组到反映它们类型的集合中 – 一个集合用于所有网格,一个用于所有灯光,一个用于曲线,等等。
由于我们已经将PythonScriptingBlender/ch3设置为我们的插件目录,我们将在 VS Code 中继续操作:
-
在 VS Code 中选择
PythonScriptingBlender/ch3/addons。 -
通过点击新建****文件图标创建一个新文件。
-
将新文件命名为
object_collector.py。 -
通过双击打开文件。
这个 Python 脚本的名称以object开头,因为它影响对象数据。这是一个软约定,因为这个文件名方案是建议的,但不是强制的。
在这个阶段,插件与之前的非常相似 – 我们还没有添加任何代码。注意,除了明显的名称和描述差异之外,我们还没有添加warning条目 – 我们打算制作一个非实验性插件:
object_collector.py
bl_info = {
"name": "Collector",
"author": "John Doe",
"version": (1, 0),
"blender": (3, 00, 0),
"description": "Create collections for object types",
"category": "Object",
}
def register():
# this function is called when the add-on is enabled
pass
def unregister():
# this function is called when the add-on is disabled
pass
请小心删除!
最好不要使用删除按钮删除从脚本路径加载的插件 – 我们可能会删除我们的工作(可能也是唯一的)副本!
Blender 将在首选项面板中显示此插件。为了添加功能,我们的插件必须包含一个操作符。操作符是执行代码的实体;我们现在将学习如何编写它们。
操作符
Operator类允许从图形界面调用函数。它们本质上是可以运行在 Blender 中的命令。
因此,我们通过子类化bpy.types.Operator类来使我们的代码对用户可用。
操作符要求
从bpy.types.Operators派生的类必须实现这些成员:
-
一个名为
bl_idname的静态字符串,其中包含操作符在内部使用的唯一名称。 -
一个名为
bl_label的静态字符串,其中包含操作符的显示名称。 -
一个
poll()类方法,用于验证执行操作的条件是否满足,并返回True或False -
一个
execute()方法,当操作符执行时运行,返回一组可能的运行状态。 -
可选的,一个 docstring,Blender 将显示为附加信息。
我们将填写这些信息,以便我们的插件包含一个可以执行的操作符。
编写基本操作符
让我们开始创建我们的操作符类。遵循 Blender 指南,名称以OBJECT_OT开头。在可选的 docstring 之后,是bl_idname和bl_label,这两个属性分别是 Blender 用作操作符标识符和描述的属性:
class OBJECT_OT_collector_types(bpy.types.Operator):
"""Create collections based on objects types"""
bl_idname = "object.pckt_type_collector"
bl_label = "Create Type Collections"
@classmethod
def poll(cls, context):
return False
def execute(self, context):
# our code goes here
return {'FINISHED'}
在这个阶段,poll()和execute()方法既不允许也不执行任何操作。我们将在接下来的几页中实现它们,使用我们在第二章中学习的内容,当处理 Blender 数据时。
实现 poll()方法
poll()验证运行操作符的条件是否满足。这限制了错误的可能性,并使操作符的预期用途更加明显。此方法使用@classmethod装饰器标记,允许我们在操作符运行之前验证条件。
由于我们的操作符收集场景中的对象,如果场景为空,我们不应能够使用它:
@classmethod
def poll(cls, context):
return len(context.scene.objects) > 0
实现 execute()方法
在操作符被调用后,Blender 运行其execute()方法。这个execute()函数包含我们的操作。将它们分解成单个步骤将有助于用 Python 编写代码。
规划我们的执行过程
我们必须知道执行我们的操作符时预期会发生什么。例如,在默认场景上运行它,我们最终会得到三个新的集合——网格用于立方体对象,相机用于相机对象,灯光用于灯光对象。

图 3.6:运行收集器后的预期结果
达到这个结果的方式不止一种,但要通过手工完成,我们需要创建网格、灯光和相机集合,并将每个对象置于每个集合之下。
现在,我们将这些操作转换为 Python 代码。
编写执行代码
我们在第二章中看到了如何创建新的集合并将其链接到scene.collection.children:
def execute(self, context):
mesh_cl = bpy.data.collections.new("Mesh")
light_cl = bpy.data.collections.new("Light")
cam_cl = bpy.data.collections.new("Camera")
context.scene.collection.children.link(mesh_cl)
context.scene.collection.children.link(light_cl)
context.scene.collection.children.link(cam_cl)
然后,我们可以使用for循环来处理对象:
for ob in context.scene.objects:
if ob.type == 'MESH':
mesh_cl.objects.link(ob)
elif ob.type == 'LIGHT':
light_cl.objects.link(ob)
elif ob.type == 'CAMERA':
cam_cl.objects.link(ob)
最后,当我们退出函数时,我们总是返回一个操作状态:
return {'FINISHED'}
此操作符仍在开发中,需要进一步完善,但我们已经可以使用它。为此,我们必须使用bpy.utils.register_class()函数通知 Blender 其存在。
在我们的插件中加载操作符
当启用时,我们的插件向 Blender 添加一个操作符,并在禁用时将其移除。这是通过调用插件中的register()和unregister()函数内的bpy.utils.register_class()和bpy.utils.unregister_class()函数来完成的:
def register():
bpy.utils.register_class(OBJECT_OT_collector_types)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_collector_types)
启用收集器插件将创建类型集合添加到 Blender 中,并允许您从用户界面调用它。
运行我们的插件
即使我们还没有添加任何图形元素,我们的附加组件也准备好首次启动。我们可以使用两个技巧来运行尚未列出的附加组件,这在开发中相当常见。
刷新附加组件列表
由于我们已添加一个新的脚本文件夹并刚刚更改了其内容,我们需要重新启动 Blender 或刷新附加组件信息。为此,我们可以在 附加组件 预设窗口的右上角点击 刷新 按钮。

图 3.7:从项目文件夹加载的 Collector 附加组件
如果我们在过滤器栏中开始输入我们附加组件的名称,列表中的条目将缩小,直到 收集器 变得容易找到并启用。现在,是时候通过 Blender 源栏 执行我们的操作符了。
从搜索工具栏运行
不属于任何图形元素的操作符是用于内部使用的 – 也就是说,可以被其他操作符调用,但不能被用户调用。
要使每个操作符可搜索,请确保在 首选项 | 界面 选项卡中启用了 开发者额外,就像我们在第二章中做的那样。如果此选项处于活动状态,以下是我们可以调用我们的操作符的方法:
-
按下 F3 按钮。
-
开始键入
create type,操作符将在搜索框中显示。

图 3.8:创建类型集合操作符,显示在搜索栏中
- 点击操作符以执行它。
我们可以在大纲视图中看到我们的操作符已成功执行。

图 3.9:每个对象都按其类型集合分组
我们的附加组件处于早期阶段;它有一些错误和限制,我们将修复。例如,网格、灯光和相机集合在没有检查它们是否已经存在的情况下创建,这将创建重复项。此外,我们只处理这三个类别,完全跳过了曲线、骨架和所有其他对象类型。
尽管如此,如果我们为我们文件夹使用版本控制,如第一章中所示,我们可以提交我们的新文件。我们将在下一节中改进我们的附加组件。
优化我们的代码
在开发中,修复错误或从稍后阶段完成的行原型开始是常见做法。在本节中,我们将完成我们的附加组件,并在 Blender 中重新加载它,并处理脚本路径的版本控制。
自动保存我们的编辑
自动保存 选项将使 VS Code 自动将每个文件更改保存到磁盘。要激活此选项,请按照以下步骤操作:
-
在 Visual Studio Code 菜单栏中打开 文件 菜单。
-
点击 自动保存 以启用此条目。
有些开发者更喜欢手动保存以更好地控制他们的文件。哪种解决方案更好取决于个人品味和工作流程。一般来说,如果使用版本控制,自动保存的优势超过了不希望更改的危险。
在某些情况下,我们希望关闭特定文件的版本控制。例如,有些文件是 Python 在执行代码时生成的;我们对此没有兴趣跟踪。在接下来的段落中,我们将看到如何忽略特定文件。
忽略字节码文件(.pyc)
如果我们从开发文件夹中执行代码,.pyc文件,以及我们的.py文件。

图 3.10:可以看到一个临时的.pyc 文件与我们的脚本并列
当一个.py文件被执行时,Python 将其转换为内部格式并保存为.pyc。我们不需要关心.pyc文件,通常我们也不需要跟踪它们。
创建.gitignore 文件
一个名为.gitignore的文本文件,包含我们不希望跟踪的文件和目录的名称,当放置在版本控制管理的文件夹中时将立即生效。我们可以手动创建它,或者遵循以下步骤在 VS Code 中操作:
-
在更改下列出的
.pyc文件中。 -
从上下文菜单中选择添加到 .gitignore。

图 3.11:在 VS Code 中添加到 git 忽略列表
-
一旦
.gitignore文件创建,.pyc文件就不再显示在更改中。 -
如果我们打开
.gitignore文件,我们会看到它包含了.pyc文件的完整路径:
ch3/addons/__pycache__/object_collector.cpython-39.pyc
-
我们不需要忽略那个特定的文件;我们可以将所有名为
__pycache__的目录列入黑名单。为此,我们采取以下代码:ch3/addons/__pycache__/object_collector.cpython-39.pyc
然后将其更改为以下内容,然后保存:
__pycache__
版本控制适用于.gitignore文件本身;我们必须将此文件与其他章节中做出的更改一起暂存和提交。

图 3.12:暂存本章当前更改
一旦我们提交了我们的更改,我们就可以回到我们的脚本工作,修复其流程,并扩展其功能。我们将看到简化脚本逻辑如何同时提高可读性、行为和功能。
修复操作符逻辑
我们操作符中最明显的流程是它试图重新创建现有的集合。连续运行两次会创建Mesh.001和Light.001集合,等等。

图 3.13:创建了不想要的集合
避免重复集合
我们应该只在它不存在的情况下创建网格集合。注意以下内容:
mesh_cl = bpy.data.collections.new("Mesh")
而不是那样,我们应该创建一个新的,只有当查找它时才会引发KeyError错误:
try:
mesh_cl = bpy.data.collections.new['Mesh']
except KeyError:
mesh_cl = bpy.data.collections.new("Mesh")
为了更通用,我们可以编写一个函数,该函数接受集合名称作为参数。
以下代码块中展示的函数以一个非常描述性的文档字符串开头,这有助于更好地了解函数应该做什么以及如何实现它:
def get_collection(name):
'''Returns the collection named after the given
argument. If it doesn't exist, a new collection
is created and linked to the scene'''
try:
return bpy.data.collections[name]
except KeyError:
cl = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(cl)
return cl
查询对象类型
我们可以使用前面的函数创建唯一的集合 - 例如,get_collection("Mesh") - 但我们不需要明确提及对象类型;Object.type 参数以字符串形式返回类型:
>>> bpy.data.objects['Cube'].type
'MESH'
字符串也可以通过它们的 .title() 方法进行格式化:
>>> bpy.data.objects['Cube'].type.title()
'Mesh'
这是重写后的操作符执行块:
@staticmethod
def get_collection(name):
'''Returns the collection named after the given
argument. If it doesn't exist, a new collection
is created and linked to the scene'''
try:
return bpy.data.collections[name]
except KeyError:
cl = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(cl)
return cl
def execute(self, context):
for ob in context.scene.objects:
cl = self.get_collection(ob.type.title())
cl.objects.link(ob)
return {'FINISHED'}
这个版本更优雅,支持任何类型的对象。尽管如此,我们仍将很快修复一个错误。在我们到达那里之前,我们需要重新加载脚本以使用这个新版本。
重新加载脚本
Blender 和 Python 将使用的脚本存储在内存中;因此,对代码所做的更改不会立即生效。有一个 Blender 命令可以重新加载脚本,我们可以在搜索栏中查找:
-
按下 F3 键进入搜索栏。
-
在搜索字段中开始键入
reload scr。 -
点击操作符,script.reload · 重新加载脚本。

图 3.14:调用重新加载脚本操作符
这个命令重新加载所有脚本,并使我们免于每次都需要重新启动 Blender。我们的插件现在使用磁盘上的最新 .py 文件,并且我们可以验证我们的集合只创建一次。
避免重新赋值错误
在执行 RuntimeError 错误时。如果我们第二次运行我们的操作符,我们会看到这个错误出现:
cl.objects.link(ob)
RuntimeError: Object 'Cube' already in collection 'Mesh'
我们需要将对象链接放在 try/catch 语句中,以避免以下情况:
cl.objects.link(ob)
这应该替换为以下内容:
try:
cl.objects.link(ob)
except RuntimeError:
continue
这样,对于已经收集的对象不会采取任何操作,操作符将继续处理场景中的其余部分。
不要太过努力!
我们应该始终确保 try 块内的操作尽可能少 - 这些语句不应轻易使用。没有明显的规则,但如果我们在一个块中尝试超过两行,我们可能需要重新思考我们的代码,使其更不容易出错。
我们最终的操作符
我们可以通过从视图中调用 添加 菜单或使用 Shift + A 快捷键来向场景添加更多对象。我们可以添加不同类型的对象,例如 文本、说话者、空 | 平面坐标轴,甚至一些新的网格,如 圆柱体 和 球体,然后再次运行 创建类型集合。我们可以看到每个对象都被分配给了以其类型命名的集合。

图 3.15:每个对象类型都有自己的集合
好处在于我们不必手动为所有对象类型进行计数——一旦程序化工作流程就位,它将适用于所有类型的对象,甚至包括 Blender 未来版本中添加的对象。
我们的操作符已经完整;缺少的是一种调用它的简单方法。我们将通过学习如何在界面菜单中显示操作符来结束本章。
扩展菜单
菜单具有许多优点——它们在应用程序中无处不在,它们涵盖了 3D 工作流程的特定方面,并且可以轻松添加新项目。我们将在我们的附加组件中处理新菜单项的添加和删除——我们的操作符只有在我们的附加组件启用时才会显示。
绘制函数
Blender 菜单以函数的形式接受新项目。这些函数描述了菜单应该如何绘制新条目;它们必须接受由菜单传递的self和context参数,并具有以下形式:
def draw_menu_item(self, context):
row = self.layout.row()
我们将在第五章中更好地掌握 UI 元素。现在,我们只会将我们的操作符添加到菜单行。我们的函数将如下所示:
def draw_collector_item(self, context):
row = self.layout.row()
row.operator(OBJECT_OT_collector_types.bl_idname)
我们现在可以将这个函数添加到 Blender 菜单中,并让它显示我们的操作符。
添加菜单项
Blender 菜单存储在bpy.types命名空间中。按照惯例,菜单类型的名称遵循以下方案:
bpy.types.[AREA]_MT_[NAME]
例如,3D 视图中的菜单以bpy.types.VIEW3D_MT_开头。在 Python 控制台中输入并按Tab键将显示视口中可用的菜单作为建议:
>>> bpy.types.VIEW3D_MT_
add(
angle_control(
armature_add(
…
由于bpy.types.VIEW3D_MT_object菜单:
>>> bpy.types.VIEW3D_MT_object
(
_animation(
_apply(
_asset(
…
_context_menu(
bpy.types.VIEW3D_MT_object是VIEW3D_MT_pose_context_menu。我们在我们的示例中使用这个,但我们可以非常容易地使用任何其他菜单。
append()和remove()方法将绘制函数添加到或从菜单中删除。这可以在我们的附加组件的register()/unregister()函数中完成,因此它变成了以下形式:
def register():
bpy.utils.register_class(OBJECT_OT_collector_types)
menu = bpy.types.VIEW3D_MT_object_context_menu
menu.append(draw_collector_item)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_collector_types)
menu = bpy.types.VIEW3D_MT_object_context_menu
menu.remove(draw_collector_item)
重新加载脚本,并在对象模式下右键单击菜单显示我们的选项在底部。现在,我们有了一种在 UI 中调用我们的操作符的方法,我们可以认为我们的附加组件已经完成,并提交我们的更改。

图 3.16:添加到上下文菜单中的我们的操作符
摘要
在本章中,我们编写了一个完整的附加组件,它扩展了 Blender 的功能,并无缝集成到应用程序中。我们还学习了如何在代码被使用的同时对其进行工作,并通过连续的细化步骤改进我们的工具。
在*第四章**中,我们将学习如何通过 Python 影响 Blender 对象的定位和旋转,并将交互式属性添加到我们的操作符中。
问题
-
Python 脚本和 Blender 附加组件之间的区别是什么?
-
使用附加组件相比稀疏代码有哪些优势?
-
操作符有什么作用?
-
我们如何定义操作符可以执行的条件?
-
我们能否在插件被使用时进行修改?我们如何更新它?
-
我们如何在 Git 版本控制中忽略字节码(
.pyc)文件? -
我们如何避免创建重复项?
第四章:探索对象变换
在空间中改变对象的位置、旋转和尺寸是任何动画软件的基本原则。
艺术家用于更改变换通道的值以执行这些操作。更技术性的用户了解此类动作的几何含义。
在本章中,我们将学习对象变换的工作原理以及如何在我们的脚本中实现它们。我们还将学习如何以编程方式添加对象约束,以及 Blender 如何为我们执行更复杂的操作。
最后,我们将实现一个新的命令,该命令可以一次影响更多对象的变换,并接受用户输入。
本章将涵盖以下关键主题:
-
使用坐标表示变换对象,并避免陷阱
-
应用对象约束和层次结构
-
使用矩阵表示
-
向我们的插件添加交互式操作员
技术要求
我们将使用 Blender 和Visual Studio Code(VS Code)。
本章创建的示例可以在以下网址找到:github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch4.
在空间中移动对象
三维对象可以移动、旋转和缩放。由于它们不改变对象的几何形状,位置和旋转被认为是刚性变换。技术上,使用缩放值改变对象的大小应用非刚性变换,但由于顶点几何形状没有改变,缩放被认为是对象级别的变换,并显示在位置和旋转旁边。
在本节中,我们将使用 Python 在 Blender 中变换对象。
变换对象
对象通过改变其位置、旋转和缩放通道的值进行变换。位置和缩放坐标立即与笛卡尔空间的 X、Y 和 Z 相关联;旋转有更多选项,因为它们带来一些含义。
影响对象的定位
我们已经在第一章和第二章中遇到了location属性。如果我们有一个活动对象,例如 Blender 默认场景中的立方体,以下行将把它移动到具有x、y、z坐标1.0、2.0和3.0的位置。这些行使用元组赋值一次设置所有三个坐标:
import bpy
bpy.context.object.location = 1.0, 2.0, 3.0
由于 Blender 坐标也可以通过字母赋值,该表达式等同于以下内容:
import bpy
bpy.context.object.location.xyz = 1.0, 2.0, 3.0
坐标存储在向量中。向量分量可以单独访问:
import bpy
bpy.context.object.location[0] = 1.0
bpy.context.object.location[1] = 2.0
bpy.context.object.location[2] = 3.0
或者,它们可以通过以下方式访问:
import bpy
bpy.context.object.location.x = 1.0
bpy.context.object.location.y = 2.0
bpy.context.object.location.z = 3.0
位置和缩放都存储为向量。
影响对象缩放
与位置一样,缩放的三维维度通过Vector坐标访问:
>>> bpy.context.object.scale
Vector((1.0, 1.0, 1.0))
我们可以为对象分配非均匀缩放,例如每个轴有不同的值:
import bpy
bpy.context.object.scale.xyz = 3.0, 2.0, 1.0
物体的缩放通常是均匀的,这意味着它在每个轴上都是相同的。Blender 矢量提供了一个方便的方式来分配均匀值:
import bpy
bpy.context.object.scale.xyz = 3.0
与位置一样,缩放坐标可以单独设置或一起设置。顺便说一下,静止时的缩放值是[1.0, 1.0, 1.0]而不是[0.0, 0.0, 0.0]。这反映了缩放是一种乘法操作,而位置是加法操作。
旋转的组合不如立即。我们将看到有不同方式来表示旋转。
影响物体旋转
使用旋转,我们可以围绕x、y和z轴对物体进行定位。这样做会给物体赋予自己的方向,并拥有自己的x、y和z局部轴。
我们将这些与物体对齐的新轴称为局部方向。Blender 允许用户在变换物体时选择不同的轴。与视口网格对齐的轴是全局方向或世界轴:

图 4.1:静止和旋转后的物体旋转环和轴
即使物体已经旋转,我们仍然可以在视口顶栏中更改旋转模式并使用全局方向:

图 4.2:仍然使用全局方向的旋转飞机
旋转比平移和缩放更复杂:根据定义,旋转涉及一个支点和恒定的距离。因此,旋转会受到一些需要额外考虑的次要问题的影响。
与旋转相关的问题
由于它们的复合性质,围绕一个轴的旋转可能会改变另一个轴的旋转值。总结一下,旋转被描述为只有两个自由度的三维属性,因为不超过两个通道有机会自由改变。
此外,旋转叠加的顺序会改变结果。为了正确可视化旋转后的物体,我们需要知道哪个轴首先被旋转,即旋转顺序。
我们可以坚持使用一种旋转顺序,比如x,然后y,然后z,但这将限制我们对抗另一个潜在缺陷的选择:重叠一个轴的另一个轴的三维旋转最终会使一个坐标变得无用,这是一个众所周知的问题,称为万向节锁。
由于不同的旋转顺序会在不同的角度上锁定,改变顺序有助于应对这个问题。
这些问题并不仅限于 Blender 或其他动画软件;它们是三维空间固有的属性,包括我们所在的空间。
为了解决这些问题,旋转提供了更广泛的选择。除了三个角度的不同组合顺序外,还有如四元数这样的抽象表示。这种术语一开始可能听起来很吓人,但随着我们继续本章的学习,它将变得更加熟悉。
更改旋转模式
变换属性中的旋转模式框显示了可用于旋转对象的可用选项:

图 4.3:旋转模式
我们在这里不需要全面覆盖这个主题,但基本上以下提供了一个简要的介绍:
-
四元数:这些是使用四个系数的数学符号:在 Blender 中,W、X、Y 和 Z。四元数不受万向节锁的影响。
-
欧拉角:这些列出三个旋转轴上的角度。这是旋转的普遍接受方式,但有两个注意事项:结果取决于轴的顺序,一个轴可能最终与另一个轴重叠。为了减轻丢失通道到万向节锁的危险,允许更多的 X、Y 和 Z 组合。
-
轴角:这使用 X、Y、Z 来定义一个点作为旋转轴。W 属性是该方向上的扭转角。

图 4.4:四元数、欧拉和轴角模式的旋转属性
在界面中更改此属性会更改显示的通道。在 Python 中,我们需要根据当前模式使用不同的属性。
在 Python 中访问旋转
rotation_mode 属性指定正在使用哪个系统来旋转对象。它是一个 TypeError。错误信息打印出允许的关键词:
>>> import bpy
>>> bpy.context.object.rotation_mode = "this won't work"
TypeError: bpy_struct: item.attr = val: enum "this won't work" not found in ('QUATERNION', 'XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX', 'AXIS_ANGLE')
每种模式都提供其设置旋转的属性:
-
QUATERNION:要影响四元数的系数 W、X、Y 和 Z,我们分别使用以下内容:-
rotation_quaternion.w或rotation_quaternion[0] -
rotation_quaternion.x或rotation_quaternion[1] -
rotation_quaternion.y或rotation_quaternion[2] -
rotation_quaternion.z或rotation_quaternion[3]
-
-
XYZ、XZY、YXZ、YZX、ZXY和ZYX是按不同顺序评估的欧拉角。无论我们选择哪一个,这些欧拉属性如下:-
rotation_euler.x或rotation_euler[0] -
rotation_euler.y或rotation_euler[1] -
rotation_euler.z或rotation_euler[2]
-
我们应该将这些值设置为弧度。
-
AXIS_ANGLE:轴角 – 轴角 (W+XYZ),定义围绕由 3D-向量定义的某个轴的旋转。我们可以通过以下方式设置扭转角(以弧度为单位):rotation_axis_angle[0]
我们可以使用以下方式设置轴向量的 x、y、z 坐标:
-
rotation_axis_angle[1] -
rotation_axis_angle[2] -
rotation_axis_angle[3]
math 模块在以弧度作为角度单位的使用上提供了一些帮助。
使用弧度和度
Blender 的 API 使用弧度而不是度来描述旋转角度。度数使用介于 0 到 360 之间的值来表示一个角度,而弧度介于 0 到 2π之间。希腊字母π(pi)指的是圆与其直径的比例。2π(约等于 6.28)测量半径为 1.0 的完整圆的弧长。
在 Python 中,我们可以使用math模块的函数在两个系统之间进行转换,即radians()和degrees(),以及pi变量以快速访问π的值。
以下是一个示例:
>>> from math import radians, degrees, pi
>>> degrees(2 * pi)
360.0
>>> radians(360)
6.283185307179586
考虑到这一点,当我们设置旋转时,我们可以即时转换角度单位。
设置旋转属性
在脚本中设置旋转之前,我们必须确保我们使用正确的旋转系统。在以下片段中,我们事先设置了旋转模式:
import bpy
ob = bpy.context.object
# apply a 90 degrees on X axis rotation using Quaternions
ob.rotation_mode = 'QUATERNION'
ob.rotation_quaternion.w = 0.707107
ob.rotation_quaternion.x = 0.707107
ob.rotation_quaternion.y = 0.0
ob.rotation_quaternion.z = 0.0
# apply a 90 degrees on X axis rotation using Eulers
ob.rotation_mode = 'XYZ'
ob.rotation_euler.x = radians(90)
ob.rotation_euler.y = 0.0
ob.rotation_euler.z = 0.0
# apply a 90 degrees on X axis rotation using Axis Angle
ob.rotation_mode = 'AXIS_ANGLE'
ob.rotation_axis_angle[0] = radians(90)
ob.rotation_axis_angle[1] = 1
ob.rotation_axis_angle[1] = 0
ob.rotation_axis_angle[1] = 0
当我们更改rotation_mode时,Blender 会将当前状态转换为所选系统。这防止了对象突然在空间中改变其方向,并且适用于大多数情况,但也有一些例外。例如,动画为每个关键帧设置了值,因此切换动画控制的旋转类型会导致在播放时改变视觉旋转。在这种情况下,我们可以在脚本中使用转换方法,就像我们将在下一个 Python 片段中看到的那样。
在旋转系统之间转换
在以下片段中,我们从一个Euler旋转开始,并使用转换方法来更改旋转模式:
from mathutils import Euler
# create a 90 degrees rotation Euler
rot_90x_eu = Euler((radians(90), 0, 0))
# convert to quaternion
rot_90x_quat = rot_90x_eu.to_quaternion()
# convert to axis angle
rot_90x_aa = rot_90x_quat.to_axis_angle()
在撰写本文时,欧拉表示法还没有to_axis_angle()方法,因此我们首先将其转换为四元数。使用四元数作为交叉点是常见的,因为它们是表达旋转的最通用系统。
旋转也可以写成矩阵的形式。矩阵形式是所有变换在内部存储的方式。在我们学习了更多关于间接变换(即在不改变其通道的情况下移动对象)之后,我们将了解这一点。
间接变换对象
我们已经看到了如何通过直接改变通道来变换对象。还有两种其他方式可以影响对象的位置、旋转和缩放。对象约束是特殊的工具,通过限制某些值或从另一个对象复制它们来影响变换。
然后还有通过父子关系安排更多对象层次结构的可能性,即通过使一个对象属于另一个对象。
我们将看到这些操作如何在 Python 中体现。
使用对象约束
约束可以移动、旋转或缩放一个对象,而不会改变其变换属性。其中一些,例如复制变换,完全覆盖了对象的变换;而另一些,例如限制距离,则在其之上操作。

图 4.5:Blender 约束菜单
大多数约束将更多对象的变换绑定在一起,例如复制位置,而另一些,例如限制位置,有自己的变换属性。
一个对象可以有未指定的约束数量。在 Python 中添加它们的步骤与在图形界面中的工作方式非常相似。
在 Python 中添加约束
约束作为对象的一个集合属性公开。可以通过向new(type)方法提供约束类型来添加它们。
与旋转模式类似,提供错误的关键字将提示错误并列出可用选项:
>>> import bpy
>>> bpy.context.object.constraints.new("this won't work")
TypeError: ObjectConstraints.new(): error with keyword argument "type" - enum " this won't work " not found in ('CAMERA_SOLVER', 'FOLLOW_TRACK', 'OBJECT_SOLVER', 'COPY_LOCATION', 'COPY_ROTATION', 'COPY_SCALE', 'COPY_TRANSFORMS', 'LIMIT_DISTANCE', 'LIMIT_LOCATION', ...
new方法返回创建的约束,因此我们可以轻松访问其属性。
设置约束属性
不同的约束类型有不同的属性,但存在一些共同的模式。大多数约束将包含以下属性:
布尔开关:
-
.enabled: 这启用/禁用约束 -
.use_x,.use_y,.use_z: 当可用时,仅启用/禁用一个轴 -
.use_offset: 当可用时,将约束效果累加到变换通道 -
对象:如果可用,使用
.target来设置约束的绑定目标 -
字符串:如果可用,使用
.subtarget来仅使用目标的一部分(例如,顶点组)进行实际计算 -
枚举开关:
.target_space: 这使约束在.owner_space上起作用:这更改约束源数据为本地、世界或自定义****对象级别
浮点数:使用.influence来传达只有部分效果
一些属性是特定于每种类型的,例如距离约束的distance属性。在这种情况下,它们的路径可以通过在图形界面中悬停或右键单击来追踪(见第二章中的复制数据路径部分)或从 API 文档中(见第二章中的访问 Blender 数据部分)。
限制对象缩放
以下代码片段添加了一个限制缩放约束,该约束限制了活动对象的最高高度:
import bpy
ob = bpy.context.object
limit = ob.constraints.new(type='LIMIT_SCALE')
limit.use_max_z = True # limit the height only
limit.max_z = 0.5
如果应用于默认立方体,它将将其高度减半,就像应用了 [1.0, 1.0, 0.5] 的缩放一样,尽管其缩放值仍然是 [1.0, 1.0, 1.0]。
对象可以是层次结构的一部分。在这种情况下,它们遵循其层次树中级别更高的对象。我们将在下一节中探讨这个概念。
使用对象层次结构
视口中的对象可以作为其他对象的子对象进行排列。在这种情况下,它们将受到其父对象的平移、旋转和缩放的影响。
我们可以通过 Python 中的parent、children和children_recursive属性来访问层次关系。只有parent属性是可写的;其他两个仅用于列出。
children和children_recursive之间的区别在于后者列出了层次结构中受影响的每个对象,包括子对象的子对象以及所有后代。
此代码片段将所有现有对象依次作为父对象,然后打印一份报告:
import bpy
previous = bpy.data.objects[0]
for ob in bpy.data.objects[1:]:
# parent each object under its predecessor
ob.parent = previous
previous = ob
for ob in bpy.data.objects:
# now print out the children of each object
print(ob.name)
child_names = (c.name for c in ob.children)
print("\tchildren:", ", ".join(child_names))
child_names = (c.name for c in ob.children_recursive)
print("\tchildren recursive:", ", ".join(child_names))
print("")
在默认场景中运行该代码将产生以下结果:

图 4.6:在一个层次结构中默认重置的父级对象
这在打印输出中得到了反映:第一个对象列出了所有其他对象作为孙辈,而children_recursive和children对于最后两个没有其他后代的对象,包含相同的结果:
Camera
children: Cube
children recursive: Cube, Light
Cube
children: Light
children recursive: Light
Light
children:
children recursive:
如果我们查看视口,我们可以看到对象位置已经改变:在 Python 中设置对象的父级会立即应用一个新的参考系。为了复制这种行为,我们需要理解变换矩阵。
理解变换矩阵
位置、旋转和缩放的三维变换存储在一个矩阵中。矩阵在广义上是由行和列排列的数字表。变换矩阵通过线性代数组合。我们不会深入细节;我们只是快速看一下矩阵的含义以及我们如何在脚本中使用它。
就像其他表示一样,Blender 在mathutils模块中提供了一个Matrix类:
>>> from mathutils import Matrix
>>> Matrix()
Matrix(((1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(0.0, 0.0, 0.0, 1.0)))
包含这些默认值,其对角线条目为1.0,其他地方为0.0的矩阵代表了静止状态。换句话说,与这个矩阵关联的对象没有被移动、旋转或缩放。它被称为单位矩阵,因为它使对象保持相同的状态。
每当对象移动、旋转或缩放时,其矩阵的条目都会改变为不同的值。
访问矩阵
对象包含多个矩阵。尝试通过自动完成来访问对象矩阵会显示四个不同的属性:
>>> bpy.context.object.matrix_
basis
local
parent_inverse
world
每个条目都涵盖了一个特定的方面:
-
matrix_basis:这个矩阵包含了对象在由对象约束变换之前的本地位置、旋转和缩放。这个矩阵反映了在对象属性中显示的通道。 -
matrix_local:这个矩阵包含了一个对象的位置、旋转和缩放,省略了从父级对象继承的变换,但不包括由约束产生的变换。 -
matrix_parent_inverse:每当我们不希望一个静止的对象与其父级完全匹配时,我们会在该矩阵内添加一个偏移量。 -
matrix_world:这个矩阵包含了在全局坐标系中的最终位置、旋转和缩放,反映了对象所受的所有变换。
考虑到这一点,我们可以改进上一节中的父级片段,并保持对象位置不变。
存储对象矩阵
通过 Python 设置父级关系之前,曾经使每个对象都瞬间移动到其父级位置。
我们希望它们在层次结构改变后保持其视觉变换。用矩阵术语来说,我们想要保持它们的世界矩阵不变。为了做到这一点,我们将学习如何正确地存储矩阵。
复制矩阵
包含单个值的 Python 变量存储它们自己的数据。具有聚合值(如列表、字典和 Blender mathutils 类型)的变量指向它们值的共享引用。
让我们看看以下示例。b 变量的值与 a 相同。在将 a 更改为 5 之后,b 仍然是 4:
>>> a = 4
>>> b = a
>>> a += 1
>>> print(b)
4
这同样不适用于列表,即使它们只包含一个元素:
>>> a = [4]
>>> b = a
>>> a[0] += 1
>>> print(b)
[5]
尽管是不同的变量,b 列表指向与 a 列表相同的数据。为了使其不更新原始数据,我们必须明确声明它是一个副本。
熟练的 Python 用户非常清楚如何使用 Python 的 copy 模块来避免这种情况。Blender 聚合类型提供了一个 .copy() 方法以方便使用。
在下面的代码片段中,对 matrix_a 的更改也会影响 matrix_b:
# two variables pointing to the same matrix
matrix_b = matrix_a # matrix_b ALWAYS equals matrix_a
下面的代码创建了一个 matrix_a,即它的所有值都是复制的:
# deep copy of a matrix
matrix_b = matrix_a.copy() # matrix_b stores its values
我们现在可以保持对象的世界变换,并在层次结构改变后恢复它们。
使用世界矩阵恢复变换
由于 matrix_world 是可写的,它可以在设置父级后存储并重新应用。
要恢复矩阵到其原始状态,我们需要存储其值的副本,如下面的代码片段所示:
import bpy
previous = bpy.data.objects[0]
for ob in bpy.data.objects[1:]:
# store a copy of the world mat
w_mat = ob.matrix_world.copy() # .copy() is important!
# parent each object under its predecessor
ob.parent = previous
# restore world position
ob.matrix_world = w_mat
# set current object as parent of the next
previous = ob
我们可以看到对象保持其位置。如果我们查看变换通道,我们会发现它们已经改变。
设置世界矩阵会影响位置/旋转/缩放值。在它们的原始位置,对象仍然回到其父级中心。
如果这不是我们想要实现的目标,我们可以使用 matrix_parent_inverse 属性来偏移原始位置。
使用父级逆矩阵创建原始偏移
parent_matrix_inverse 属性包含一个从界面中隐藏的变换。它用于设置远离父级原点的原始位置。
策略是抵消继承的变换,将它的逆变换添加到变换中。例如,将对象移动到 [5.0, 5.0, 5.0] 坐标的逆变换是将它移动到 [-5.0, -5.0, -5.0]。
逆转旋转稍微复杂一些,但在 Blender 中,我们可以使用其矩阵的 .inverted() 方法找到任何变换的逆变换。
这就是以下代码片段如何将 bpy.data 中的对象设置为父级,同时保持它们的变换和视觉坐标:
import bpy
previous = bpy.data.objects[0]
for ob in bpy.data.objects[1:]:
# parent each object under its predecessor
ob.parent = previous
# set parent inverse offset
offset_matrix = previous.matrix_world.inverted()
ob.matrix_parent_inverse = offset_matrix
# set current object as parent of the next
previous = ob
矩阵系统可能令人畏惧,因为许多人通常不会以这种形式考虑变换。但即使是对它的基本理解也为我们提供了脚本工具箱中的非常强大的工具。
在下一节中,我们将本章学到的技能应用于单个插件。此插件可以一次性更改许多对象的位置,并且可以选择通过约束进行工作。
编写 Elevator 插件
现在我们知道了如何在 Python 中变换对象,我们可以编写一个新的插件,其中包含变换操作符。
此插件允许我们将所有选定的对象移动到一定的高度以上。当我们想要为场景设置最小高度,即地板时,这可能很有用。正如我们在第三章中所做的那样,我们从一个基本的实现开始,然后我们将继续对其进行完善。像往常一样,我们首先为章节的代码设置一个文件夹。
设置环境
正如我们在第三章开头所做的那样,我们在ch4文件夹中为第四章创建一个文件夹作为脚本文件夹:

图 4.7:第四章的系统文件夹
现在是时候向我们的项目中添加一个新文件了:
-
在 VS Code 中选择
PythonScriptingBlender/ch4/addons。 -
点击新建 文件图标创建一个新文件。
-
将新文件命名为
object_elevator.py。 -
双击文件打开它。
我们现在可以开始编写我们的插件了。
编写第一个草稿
正如我们在第三章的插件要求部分所看到的,我们的插件需要以下内容:
-
插件信息
bl_info字典 -
执行所需操作的操作员
-
register/unregister函数用于enable/disable操作
让我们开始编写第一个草稿,通过填写要求;我们可以在第二步中完善插件:
- 我们在
bl_info头部写下附加信息。这也有助于阐明工具的目的和功能:
object_elevator.py
bl_info = {
"name": "Elevator",
"author": "John Doe",
"version": (1, 0),
"blender": (3, 00, 0),
"description": "Move objects up to a minimum height",
"category": "Object",
}
-
现在,让我们确定主要功能:插件包含一个操作员,可以将所有对象移动到给定的高度。我们将这个高度存储在静态变量
floor中,目前它是硬编码的,设置为5.0:class OBJECT_OT_elevator(bpy.types.Operator):"""Move Objects up to a given height"""bl_idname = "object.pckt_floor_transform"bl_label = "Elevate Objects"floor = 5.0 -
由于它会影响选定的对象,
poll()方法中要检查的条件是选择不为空:@classmethoddef poll(cls, context):return len(bpy.context.selected_objects) > 0 -
这里是代码的主体部分:
execute函数检查每个对象的Z位置是否不小于self.floor(目前,self.floor等于5.0)。当所有对象都处理完毕后,它返回一个'FINISHED'状态:def execute(self, context):for ob in context.selected_objects:if ob.location.z > self.floor:continueob.location.z = self.floorreturn {'FINISHED'} -
现在,我们可以将我们的操作员添加到对象的右键菜单中;为此,我们需要一个
drawmenu函数:def draw_elevator_item(self, context):# Menu draw functionrow = self.layout.row()row.operator(OBJECT_OT_elevator.bl_idname) -
我们插件的全部元素都已准备就绪;剩下的只是将它们添加到注册函数中。这就是我们这样做的方式:
def register():# add operator and menu itembpy.utils.register_class(OBJECT_OT_elevator)object_menu = bpy.types.VIEW3D_MT_object_context_menuobject_menu.append(draw_elevator_item)def unregister():# remove operator and menu itembpy.utils.unregister_class(OBJECT_OT_elevator)object_menu = bpy.types.VIEW3D_MT_object_context_menuobject_menu.remove(draw_elevator_item)
我们的插件已经准备好进行测试。我们可以在插件首选项中找到它:

图 4.8:在插件首选项中启用的对象:电梯
当插件启用时,对象右键菜单中会添加一个新条目:

图 4.9:在视图中右键单击显示我们的新菜单项
如果我们选择一些对象并通过右键单击它们打开上下文菜单,我们将找到 location.z 到 5.0,除非它已经有一个更高的值。
为场景设置最小高度在它包含地面层并且我们想要确保没有对象最终低于它时很有用。然而,OBJECT_OT_elevator.floor 的静态值在这里没有帮助,因为它只适用于地面层等于 5.0 的情况。
幸运的是,这只是为了测试:脚本的最终版本使用了一个输入参数。
使用输入属性
将我们操作符中的静态 floor 成员替换为可编辑值需要 Blender 将用户输入通道到我们的 Python 脚本中。
为了这个目的,Blender 的 API 提供了特殊的属性,它们在界面中作为图形元素出现,并可以在 Python 脚本中用作变量。这些属性是 bpy.props 模块的一部分。
要使 floor 成为我们的操作符的可编辑属性:
-
由于
OBJECT_OT_elevator.floor是一个浮点数,所以我们需要使用FloatProperty:import bpyfrom bpy.props import FloatProperty -
由于我们正在强制指定一个特定类型,我们将使用 Python 的
floor = 5.0到floor:FloatProperty(name="Floor", default=0)。
注意
使用替代确定类型的变量注释是 Python 中的最佳实践,但在 Blender 中是必需的:否则输入属性将不会出现。
- 然后,我们必须记住,由于 Blender 的工作方式,接受输入值的操作符必须了解撤销系统。因此,我们添加了
bl_options = {'REGISTER', 'UNDO'}属性。
现在我们操作符的标题看起来是这样的:
class OBJECT_OT_elevator(bpy.types.Operator):
"""Move Objects up or down by given offset"""
bl_idname = "object.pckt_type_collector"
bl_label = "Create Type Collections"
bl_options = {'REGISTER', 'UNDO'}
floor: FloatProperty(name="Floor", default=0)
- 通过按 F3 + 重新加载脚本 并再次执行 提升对象 来刷新操作符,将在屏幕左下角显示一个展开的弹出窗口中的输入属性:

图 4.10:我们的可编辑地板属性
更改此属性会影响所有选中对象的最小高度。
到目前为止,我们一直在操作 location.z 属性。如果我们的对象有一个具有不同方向或缩放的父对象,这可能不起作用。我们可以通过使用对象世界矩阵来克服这个问题。
在世界矩阵中设置高度
Blender 将对象平移存储在矩阵的最后一列中,如 图 4**.11 所示:

图 4.11:变换矩阵的项
Matrix 的索引指向其行;因此,要访问位置 z,我们需要获取第三行并查找其第四个元素。由于枚举从 0 开始,我们正在寻找的索引分别是 [2] 和 [3]。
我们的 execute 函数现在使用 matrix_world[2][3] 而不是 location.z。由于矩阵值在脚本执行期间不会自动更新,我们将在值设置后调用 context.view_layer.update():
def execute(self, context):
selected_objects = context.selected_objects
for ob in selected_objects:
matrix_world = ob.matrix_world
if matrix_world[2][3] > self.floor:
continue
matrix_world[2][3] = self.floor
# make sure next object matrix will be updated
context.view_layer.update()
return {'FINISHED'}
这个版本的脚本可以处理继承父变换的对象,但如果父对象也被选中怎么办?
在处理完子对象之后移动父对象将改变两者的位置,从而将子对象带到错误的高度。
我们需要确保父对象总是首先移动。
避免重复变换
我们需要重新排序我们的对象列表,但由于context.selected_objects是只读的,我们无法直接对其进行排序;我们需要将其内容复制到一个列表中。
将选定的对象复制到可编辑列表中
我们可以使用copy模块创建该列表的浅拷贝。它将引用相同的数据,但允许我们随意对其进行排序:
from copy import copy
然后,在execute方法中,定位以下代码:
selected_objects = context.selected_objects
用以下代码替换它:
selected_objects = copy(context.selected_objects)
现在我们可以以这种方式对列表进行排序,这样就不会导致同一对象被移动两次。
按层次排序
要对一个列表进行排序,我们需要一个函数,该函数返回每个元素在新顺序中的位置。
我们希望在处理完父对象之后才处理子对象。通过重新排序列表,使具有更多祖先的对象稍后处理,以满足这个条件。
我们需要一个函数来返回祖先的数量:从一个对象开始,检查它是否有父对象,然后检查那个父对象是否有父对象,直到没有找到为止。ancestors_count函数使用while循环实现这一点:
def ancestors_count(ob):
"""Return number of objects up in the hierarchy"""
ancestors = 0
while ob.parent:
ancestors += 1
ob = ob.parent
return ancestors
我们将这个函数添加到我们的脚本中,并将其用作sort方法的key参数:
def execute(self, context):
# sort parent objects first
selected_objects = copy(context.selected_objects)
selected_objects.sort(key=ancestors_count)
for ob in selected_objects:
world_mat = ob.matrix_world
if world_mat[2][3] > self.floor:
continue
# ensure update of next object's matrix
world_mat[2][3] = self.floor
return {'FINISHED'}
我们现在可以将所有选定的对象提升到最小高度,并避免在层次结构中累加变换。
我们可以认为它已经完成,但既然我们知道如何添加约束,我们可以将其用于相同的目的。
添加约束开关
我们可以允许用户使用约束,同时不影响变换通道。我们可以这样做到:
-
由于我们想要显示一个用于使用约束的复选框,我们需要在我们的操作符中添加一个布尔属性。我们需要像之前导入
FloatProperty一样导入BoolProperty:from bpy.props import BoolProperty -
然后,我们在我们的操作符中添加一个
BoolProperty注解:class OBJECT_OT_elevator(bpy.types.Operator):"""Move Objects up or down by given offset"""bl_idname = "object.pckt_type_collector"bl_label = "Create Type Collections"bl_options = {'REGISTER', 'UNDO'}floor: FloatProperty(name="Floor", default=0)constr: BoolProperty(name="Constraints", default=False) -
当
constr属性设置为True时,我们将使用约束。默认情况下,我们将其设置为False,这样新的选项就不会改变附加组件的行为。 -
使用约束使我们的工作变得更简单;我们不需要对对象进行排序并设置它们的矩阵。我们的
execute函数现在开始如下:def execute(self, context):if self.constr:for ob in context.selected_objects:limit = ob.constraints.new('LIMIT_LOCATION')limit.use_min_z = Truelimit.min_z = self.floorreturn {'FINISHED'}# affect coordinates directly# sort parent objects first…
如果我们使用约束,我们只需退出函数,在设置完约束后立即返回一个{'FINISHED'}集合。如果不这样做,execute函数将继续执行之前的代码。
视觉结果等效,但开启约束不会影响变换通道。有一个最后的注意事项:如果操作符在相同的对象上多次运行,将添加一个新的约束。
当找到现有约束时,我们将使提升对象重用该约束。这避免了为相同目的创建过多的约束。它还防止了先前约束的效果相互干扰。当一个对象有多个位置限制时,只有最严格的那个是有效的。
避免重复约束
如果对象上存在限制位置,我们的操作符会使用它。我们将其行为设置为可选,以防用户无论如何都想创建新的约束:
-
为了做到这一点,我们首先为我们的操作符添加另一个布尔属性:
reuse: BoolProperty(name="Reuse Constraints", default=True) -
然后,在我们的循环内部,我们检查是否存在我们可以使用的现有约束。如果没有找到,我们的脚本将创建它。
这种行为可以通过一个函数实现:
def get_constraint(ob, constr_type, reuse=True):
"""Return first constraint of given type.
If not found, a new one is created"""
if reuse:
for constr in ob.constraints:
if constr.type == constr_type:
return constr
return ob.constraints.new(constr_type)
-
这使得我们的
execute方法变得更加简洁:def execute(self, context):if self.constr:for ob in context.selected_objects:limit = get_constraint(ob,'LIMIT_LOCATION',self.reuse)limit.use_min_z = Truelimit.min_z = self.floorreturn {'FINISHED'}…
如果我们重新加载脚本并运行操作符,我们将在执行面板中看到所有其属性:

图 4.12:所有提升对象选项
理想情况下,重用属性应该仅在约束启用时显示,因为否则它没有效果。
如果我们注意绘制我们工具的用户界面,这是在下一章中将要介绍的,这是可能的。
目前,我们为从非常简单的工具开始的脚本添加了大量灵活性。这使我们到达了本章的结尾,因为我们已经涵盖了关于编写自定义工具的大部分主题。
摘要
在本章中,我们学习了如何利用Vector、Quaternion和Matrix实体,以及不同的旋转模式来为我们自身谋利。这为我们理解并掌握空间中的变换以及移动场景中的对象提供了元素。
我们还学习了如何在 Python 中创建约束,这在所有设置脚本中都非常重要。
最后,我们学习了我们的操作符如何在执行期间从用户那里获取输入并在 Blender 用户界面中显示它们的参数。
在第五章中,我们将学习如何为我们自己的图形界面编写面板代码,并使其选项能够感知上下文。
问题
-
什么是刚体变换?
-
我们能否在不同旋转系统之间转换坐标?
-
我们能否在不同旋转顺序之间转换坐标?
-
为什么四元数在转换中很有用?
-
变换是以何种形式存储在内部?
-
对象是否只与一个变换矩阵相关联?
第五章:设计图形界面
许多工具都会向图形界面添加自己的元素。在前几章中,我们使用了现有的菜单,但也可以向 Blender 窗口添加新的面板。
设计界面时,我们必须决定显示哪些元素以及如何访问它们,提供哪些信息,以及允许哪些操作。
在本章中,你将学习如何将新面板插入 Blender 的不同区域,如何显示信息和图标,以及如何添加可以调用操作员的按钮。
本章将涵盖以下主题:
-
理解 Blender 界面
-
绘制自定义面板
-
上下文和 UI 交互
技术要求
在本章中,我们将使用 Blender 和 Visual Studio Code。本章创建的示例可以在github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch5找到。
示例文件包括用作图标的 2D 图像。可选地,任何 2D 软件都可以用来创建自定义的.png图像,并且你可以使用它们代替。
要实现我们的界面,我们必须了解 Blender 的结构。让我们从深入 Blender 屏幕开始,开始我们的图形界面之旅。
区域、区域和面板
Blender 窗口被分为区域。每个区域可以包含不同类型的编辑器,例如 3D 对象的视口或视频编辑的序列编辑器。每个编辑器或空间可以包含一个或多个区域。区域的数量和类型因不同类型的编辑器而异:例如,一些编辑器,如首选项窗口,有一个导航侧边栏,而其他则没有。
Blender 手册详细解释了界面:docs.blender.org/manual/en/3.1/interface/index.xhtml。
目前我们需要知道的是,区域可以包含面板,面板是文本、可编辑值和按钮等图形元素的基本容器。
我们可以使用 Python 创建新的面板,这使得轻松自定义任何区域成为可能。面板必须包含有关它所属的区域和区域的信息:

图 5.1:Blender 界面中的区域、区域和面板
在内部,面板可以访问 Blender 和场景的信息,以便它们可以显示对象的状态和属性并执行操作。现在我们更熟悉屏幕的结构,我们将创建一个帮助我们处理 3D 对象的面板。
创建一个简单面板
我们将从包含一些文本和图标的简单面板开始,并了解如何将这个初始想法扩展成一个可以帮助管理场景中对象的工具。
我们的面板是一个新的类,它从bpy.types.Panel派生。像操作符一样,面板需要设置一些静态成员;否则,它们将无法工作。与操作符类似,面板可以有一个poll()类方法,该方法声明在哪些条件下可以显示面板。
与使用execute函数不同,面板通过draw(self, context)函数设置和绘制其内容。
由于我们正在向 Blender 界面添加新内容,因此我们将在一个新插件中完成这项工作。这不是强制性的,但它使得启用和禁用我们的面板变得更加容易。
为了保持我们的代码整洁和清晰,我们将为这一章编写的脚本创建一个新文件夹。
设置环境
让我们在ch5文件夹中创建一个名为第五章的文件夹作为脚本文件夹,并重新启动 Blender。
我们的插件脚本包含一个面板,就像前几章包含操作符一样:
-
在Visual Studio Code中选择
PythonScriptingBlender/ch5/addons。 -
通过点击新建文件图标创建一个新文件。
-
将新文件命名为
simple_panel.py。 -
通过双击打开文件。
我们现在可以开始编写我们的面板插件了。
设计我们的面板插件
正如我们从第三章中了解到的,需要三个元素:
-
包含基本信息的
bl_info字典 -
一个
import bpy语句来访问 Blender API -
register()和unregister()方法分别用于启用/禁用插件。
我们还需要为我们要添加的图形元素创建一个类——在这种情况下,一个从bpy.types.Panel派生的类。
我们将从信息字典开始,并为设置框架所需的元素添加占位符,这样我们就可以编写一个完全工作的 UI 插件。
编写信息字典
bl_info字典将提供插件的name属性、其author和version、所需的blender版本,以及一个简短的description。我们还可以添加一个category,该插件将列在该类别下。以下是代码的示例:
bl_info = {
"name": "A Very Simple Panel",
"author": "John Doe",
"version": (1, 0),
"blender": (3, 2, 0),
"description": "Just show up a panel in the UI",
"category": "Learning",
}
现在,我们可以继续编写所需的import语句和主class。
设计面板类
现在我们已经导入了bpy模块,我们可以基于bpy.types.Panel编写一个类。
我们可以为我们的类使用任何名称,但 Blender 推荐一些指导原则:
-
由于我们的面板将是对象属性的一部分,类名必须以
OBJECT开头。 -
名称中间包含
_PT_,因为这是一个Panel Type。
现在,我们的类将只包含一个文档字符串和一个pass语句:
import bpy
class OBJECT_PT_very_simple(bpy.types.Panel):
"""Creates a Panel in the object context of the
properties editor"""
# still a draft: actual code will be added later
pass
在添加方法和属性之前,我们将通过注册函数处理类的激活和关闭。
面板注册
register和unregister函数在插件启用和禁用时分别将此类添加到/从 Blender 中添加/删除:
def register():
bpy.utils.register_class(OBJECT_PT_very_simple)
def unregister():
bpy.utils.unregister_class(OBJECT_PT_very_simple)
通过这样,我们已经创建了面板插件的初始结构。现在我们将添加用于显示一些文本的元素和属性。
设置显示属性
Blender 寻找遵循bl_*模式的属性以确定面板在哪里以及如何显示。面板具有与操作员相同的识别属性,正如我们在第三章中介绍Operator类时所看到的:
-
bl_label: 面板的显示名称 -
bl_idname: 面板在内部使用的唯一名称
然后,有一些属性仅用于从bpy.types.Panels派生的类:
-
bl_space_type: 面板所属的编辑器 -
bl_region_type: 要使用的编辑器区域 -
bl_context: 特定对象/模式的子区域 -
bl_category: 当可用时,区域内的标签
所有这些都是静态字符串,并且bl_space_type、bl_region_type和bl_context必须匹配 Blender 已知的屏幕区域的具体值。
可能的值包括 Blender 中所有可用的编辑器。一开始这可能会让人感到不知所措,但一旦我们有了放置面板的想法,我们就可以在在线文档docs.blender.org/api/3.2/bpy.types.Panel.xhtml中查找相关信息。
由于 Blender 包含许多编辑器,并且每个编辑器都有自己的子元素,我们将查看可能的组合。
通过bl_space_type选择我们的编辑视图
首先,我们必须决定将我们的面板添加到哪个 Blender 编辑器。这主要取决于我们工具的目的以及在哪里找到它更方便。例如,如果我们的面板有助于制作视频,它将是bl_space_type的一部分:
-
EMPTY: 此值在脚本中不使用 -
VIEW_3D: 用于操纵对象的3D 视图 -
IMAGE_EDITOR: 用于查看和编辑图像和 UV 图的UV/图像编辑器 -
NODE_EDITOR: 用于基于节点的着色和合成工具的节点编辑器 -
SEQUENCE_EDITOR: 视频序列编辑器编辑工具 -
CLIP_EDITOR: 用于运动追踪的剪辑编辑器 -
DOPESHEET_EDITOR: 用于调整关键帧时间的Dope Sheet -
GRAPH_EDITOR: 用于驱动和关键帧插值的图表编辑器 -
NLA_EDITOR: 用于组合和分层动作的非线性动画编辑器 -
TEXT_EDITOR文本编辑器用于编辑脚本和文件内的文档 -
CONSOLE: 用于交互式脚本开发的Python 控制台 -
INFO: 关于操作、警告和错误消息的信息 -
TOPBAR: 用于全局、窗口级设置的顶部栏 -
STATUSBAR: 屏幕底部的状态栏用于显示一般信息 -
OUTLINER: 大纲视图的场景树和数据块概览 -
PROPERTIES: 属性用于编辑活动对象和数据块的特征 -
FILE_BROWSER: 文件浏览器用于滚动查看文件和资产 -
SPREADSHEET: 用于在表格中探索几何数据的电子表格 -
PREFERENCES: 用于编辑持久配置设置的首选项。
一旦我们决定了空间类型,我们就可以为它选择一个区域。
通过 bl_region_type 选择区域。
区域的类型取决于我们在上一步中选择的空间。不同的编辑器有不同的区域。因此,默认值总是可用的。以下是bl_region_type的所有选项描述:
-
WINDOW: 空间区域的主区域。这是默认值。 -
HEADER: 用于菜单和按钮的小型水平条。 -
CHANNELS: 在 Blender 的旧版本中使用,为了向后兼容而保留。 -
TEMPORARY: 从主窗口分离的弹出窗口。 -
UI: 包含对象设置(通过N切换)的侧边栏。 -
TOOLS: 包含一组交互式工具的工具栏(通过T切换)。 -
TOOL_PROPS: 在模式窗口中的设置,例如文件浏览器。 -
PREVIEW: 视频序列器的预览区域。 -
HUD: 操作员的重做面板。 -
NAVIGATION_BAR: 首选项窗口中的侧边栏。 -
EXECUTE: 模式窗口中的底部栏。 -
FOOTER: 用于显示当前操作信息的栏。 -
TOOL_HEADER: 用于工具设置的小型水平条。 -
XR: 虚拟现实控制器的接口。
通过 bl_context 选择上下文
一些区域会根据当前选择、活动工具或交互模式而变化。在这种情况下,需要bl_context属性。
例如,'SCENE'、'OBJECT'和'CONSTRAINTS'。如果我们不确定使用哪一个,我们可以只是激活我们感兴趣的标签页,并检查bpy.context.space_data.context = NAME_OF_CONTEXT:

图 5.2:在选择了对象属性后,信息日志区域中的 UI 上下文名称
不遵循用户上下文但仍然允许你在标签页中分组其面板的区域提供类别属性。
使用 bl_category 在标签页中进行分组
具有任意标签页的区域将查看bl_category变量以查找正确的标签。如果没有提供值,新面板将被添加到杂项标签页。如果没有以该值命名的标签页,将创建一个新的标签页。
我们将在本章末尾使用类别属性与'VIEW_3D'空间类型结合使用。我们将从没有标签的'PROPERTIES'编辑器开始。
将面板添加到对象属性区域
将我们的面板添加到bl_space_type为'PROPERTIES'和bl_context为'object'。
Panel需要一个draw函数,实际设计就在这里进行。在这个阶段,我们可以将其留空:
import bpy
class OBJECT_PT_very_simple(bpy.types.Panel):
"""Creates a Panel in the object context of the
properties space"""
bl_label = "A Very Simple Panel"
bl_idname = "VERYSIMPLE_PT_layout"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'object'
def draw(self, context):
# add layout elements
pass
与 Blender 类的大多数运行时函数一样,draw函数接受self和context参数。根据 Python 惯例,self是类的运行实例,而context包含有关 Blender 场景当前状态的信息。
现在,让我们学习如何使用draw方法向面板布局添加元素。
绘制面板的内容
draw函数在每次面板被使用或更新时都会连续执行。因此,它不应执行任何计算密集型任务,只需负责显示的元素即可。
面板的元素根据其布局排列。由于布局是一个非静态成员,它可以在draw函数中使用self.layout来访问。
默认情况下,所有元素都垂直堆叠在一个column中,但不同类型的布局将提供不同的方式来排列row或网格中的小部件。
布局也可以嵌套在一起,以实现更复杂的排列。让我们学习如何访问主布局并向其中添加元素。
与布局一起工作
所有布局类型都源自UILayout类。它们有添加子元素或子布局的方法。完整的属性和方法列表可以在 https://docs.blender.org/api/3.2/bpy.types.UILayout.xhtml 的 API 文档中找到。
因此,要显示文本,我们可以使用UILayout.label方法。以下是我们的draw函数的前几行代码:
def draw(self, context):
layout = self.layout
layout.label(text="A Very Simple Label")
如果我们启用此附加组件并到达对象属性区域,我们将能够看到我们的新面板显示一些文本:

图 5.3:我们的面板在对象属性区域显示
显示图标
标签也可以显示图标。有两种类型的图标:
-
内置图标,这些图标随 Blender 一起提供。
label方法提供了一个icon关键词来使用它们。 -
可以通过
icon_value参数使用外部图像。
Blender 的原生图标集在整个应用程序中使用。每个图标都通过一个关键词来标识。例如,LIGHT显示一个灯泡:

图 5.4:Blender 为 LIGHT 关键字提供的图标
由于有超过 800 个内置图标,因此 Blender 包含一个用于搜索它们的附加组件。
使用图标查看器附加组件查找内置图标
搜索栏区域的图标:

图 5.5:激活图标查看器附加组件
一旦附加组件被启用,图标查看器按钮就会出现在Python控制台标题中:

图 5.6:Python 控制台标题中的图标查看器按钮
点击此按钮将打开一个窗口,显示所有原生图标。我们可以通过左键单击来选择它们:

图 5.7:图标查看器附加窗口
选择一个图标将在右上角显示相关的关键字。该关键字也将复制到剪贴板。例如,如果我们选择 问号 图标,这是在撰写本文时位于左上角的第一个图标,QUESTION 关键字将被显示,如下面的图所示:

图 5.8:QUESTION 关键字显示在右上角
我们可以在顶部中间标记有放大镜图标的筛选字段中输入搜索关键字。
例如,我们可以输入 “info” 来仅显示 'INFO' 图标。现在我们知道了它们的关键字,我们可以这样显示这些图标:
def draw(self, context):
layout = self.layout
layout.label(text="A Very Simple Label",
icon='INFO')
layout.label(text="Isn't it great?",
icon='QUESTION')
标签图标显示在文本之前,是使其突出的一种好方法:

图 5.9:在自定义面板中显示的‘INFO’和‘QUESTION’图标
内置图标始终可用,并且不需要与我们的脚本一起分发外部文件。在需要时,我们也可以使用图像文件。bpu.utils.previews 模块可以用于从图像文件加载图标并使用索引号检索它们。
使用自定义图像图标
在这个例子中,我们将添加笑脸图标到我们的面板。图像文件名为 icon_smile_64.png,可以在本章的 Git 文件夹中找到。
或者,任何存储在 .png 格式并与附加程序的 .py 文件一起的图片都将有效。在这种情况下,脚本中使用的图片文件名必须相应更改。
分辨率不应过高:一个 64 像素宽的方形图片通常就足够了:

图 5.10:一个 64x64 的笑脸
要向 Blender 添加自定义图标,我们的脚本需要导入以下内容:
-
Python 的
os模块,用于构建图标文件路径并确保它在所有平台上都能工作 -
Blender 的
bpy.utils.previews模块,用于为我们图标生成 Blender 标识符
导入这些内容后,我们的 import 部分将看起来如下:
import bpy
from bpy.utils import previews
import os
我们的图标必须在脚本中的任何地方都可以访问。我们可以使用全局变量、静态成员或单例进行存储。在这个例子中,我们使用全局变量,因为它是一个更简单的选项。
因此,紧随 import 部分之后,我们必须添加以下行:
# global variable for icon storage
custom_icons = None
我们将变量初始化为 None,因为我们可以在 register/unregister 函数内部加载和清除它。我们也可以为此添加特定的函数。这样,代码将更容易理解:
def load_custom_icons():
"""Load icon from the add-on folder"""
Addon_path = os.path.dirname(__file__)
img_file = os.path.join(addon_path,
"icon_smile_64.png")
global custom_icons
custom_icons = previews.new()
custom_icons.load("smile_face",img_file, 'IMAGE')
然后,我们需要一个函数在附加程序注销时清除 custom_icons:
def remove_custom_icons():
"""Clear Icons loaded from file"""
global custom_icons
bpy.utils.previews.remove(custom_icons)
这些函数随后在注册部分被调用:
def register():
load_custom_icons()
bpy.utils.register_class(VerySimplePanel)
def unregister():
bpy.utils.unregister_class(VerySimplePanel)
clear_custom_icons()
一旦我们读取了图片文件,我们使用 "smile_face" 作为 custom_icons.load() 的第一个参数,因此这就是用于检索其标识符的关键字。以下是用于标签的代码:
layout.label(text="Smile", icon_value=custom_icons['smile_face'].icon_id)
如果我们从F3搜索面板查找并执行重新加载脚本,我们将在面板中看到我们的自定义图标:

图 5.11:从我们的文件加载并显示在我们的面板中的笑脸图标
目前,我们已经使用了默认的列布局。我们将在下一节中学习如何使用不同的排列。
我的部件去哪里了?
接口代码中的错误会“静默”失败;也就是说,Blender 不会明显地抱怨,而是只是停止绘制有错误的面板。
这可以防止 UI 崩溃,但会使我们的代码更难调试;我们只会注意到一些小部件没有显示。
当这种情况发生时,最好的做法是检查控制台输出或脚本工作区的信息日志区域。它将包含有关哪个代码行失败的跟踪信息。
在我们的面板中使用布局
如果我们对全局布局的默认堆叠方式不满意,我们可以添加我们选择的布局类型,并使用它,我们将得到不同的排列。
例如,我们可以使用row将两个标签放在同一行上。此外,即使我们对我们元素堆叠的方式感到满意,创建一个column子布局也是良好的实践。这种做法至少有两个优点:
-
我们保留了面板的外观,即使默认排列应该改变
-
我们不会污染原始布局
让我们看看我们如何改变小部件堆叠的方式。
列和行的排列
我们可以在draw函数内部嵌套更多布局类型。例如,我们可以将上一个示例中的最后两个标签并排放置,而不是垂直排列。为此,我们必须做两件事:
-
首先,我们必须创建一个
column并将第一个标签添加到其中。 -
然后,我们必须创建一个
row。我们将添加到其中的两个标签将相邻:
def draw(self, context):
col = self.layout.column()
col.label(text="A Very Simple Label",
icon='INFO')
row = col.row()
row.label(text="Isn't it great?",
icon='QUESTION')
icon_id = custom_icons["smile_face"].icon_id
row.label(text="Smile", icon_value=icon_id)
现在,我们的面板只包含两行:

图 5.12:第二行包含两个标签
添加带有框布局的框架
其他类型的子布局提供额外的效果。例如,框布局就像一列,但它被框在平滑的矩形中。假设我们想显示一些来自附加组件的bl_info的信息。在这里,我们可以将这些行添加到draw函数中:
box = col.box()
row = box.row()
row.label(text="version:")
row.label(text=str(bl_info['version']))
在我们调用重新加载脚本后,我们将看到围绕该信息的框架:

图 5.13:包围版本信息的框布局
我们在行中放置了标题"version"和表示bl_info['version']的一些信息。这样,每个元素都有相同的空间。为了更好地控制第一个元素占用的空间,我们可以使用split布局。
使用复合布局
一些布局由更多行或列组成。split布局将可用空间分配到不同的列,而grid布局会自动创建行和列。
我们将使用它们来构建一个更复杂的面板。
分割排列
我们可以使用split方法创建一个布局,其列宽可以调整。factor参数是可选的,接受0.0到1.0之间的值。将其保留为默认值0.0将自动计算最佳宽度;否则,它将设置第一列占用的百分比。
在这个例子中,我们将使用0.33这个系数来减少第一列的空间。在这里,我们还将创建两列,以便我们稍后填充它们并排列更多元素,就像在表格中一样。
下面的代码片段每行显示两个条目。第一列大约占据三分之一的空間:
# ...
box = col.box()
split = box.split(factor=0.33)
left_col = split.column()
left_col.label(text="author:")
left_col.label(text="version:")
right_col = split.column()
right_col.label(text=str(bl_info['author']))
right_col.label(text=str(bl_info['version']))
重新加载脚本后,我们将看到我们的标题占据了三分之一的空间,其余空间留给相关信息:

图 5.14:作者和版本信息占据三分之一的空間
我们可以利用字典方法从bl_info中添加更多信息。这样,我们可以使用for循环来填充我们的split布局。
使用字典填充
由于我们已经创建了列,我们可以使用循环添加更多条目。这对于在字典中显示条目非常理想。
假设我们想显示所有插件信息。在这种情况下,我们可以使用items()方法迭代所有关键字/值对:
# …
box = col.box()
split = box.split(factor=0.3)
left_col = split.column()
right_col = split.column()
for k, v in bl_info.items():
if not v:
# ignore empty entries
continue
left_col.label(text=k)
right_col.label(text=str(v))
在这里,我们使用continue跳过bl_info的未设置值,当v为空时。在这几行中,我们可以显示所有可用的插件信息:

图 5.15:显示 bl_info 的框布局
如果我们愿意让 Blender 决定列宽,我们可以使用网格布局。
排列网格
grid_flow布局非常适合将我们的元素排列成表格,因为它会自动创建行和列。例如,我们可以通过使用grid_flow(columns=2)和将标签添加到for循环中来在两列中显示场景中的对象名称:
# ...
col.label(text="Scene Objects:")
grid = col.grid_flow(columns=2)
for ob in context.scene.objects:
grid.label(text=ob.name)
此代码将显示当前场景中对象的名称,以两列网格排列:

图 5.16:以网格显示的对象名称
通过这样,我们已经看到标签也可以显示图标。这意味着我们可以在每个名称旁边显示对象的类型图标,就像大纲视图一样。
构建图标关键词
在OUTLINER_OB_MESH和OUTLINER_OB_CURVE中进行快速搜索,遵循以下模式:
OUTLINER_OB_[OBJECT_TYPE]
这在下图中表示:

图 5.17:在图标查看器区域显示的对象类型图标
考虑到这一点,我们可以使用 字符串格式化(Python 3 的一个特性,使得组合字符串和变量更容易)来构建这些关键字。为了通知 Python 我们正在使用格式化,我们必须在引号或撇号分隔符之前放置一个 f 字符,然后在字符串内部用花括号包围我们的变量。以下是一个示例:
>>> h = "Hello"
>>> print(f"{h}, World!")
Hello, World!
考虑到这一点,我们使用 ob.type 属性获取对象类型的字符串 - 例如,'MESH'、'CURVE' 或 'ARMATURE' - 然后使用以下行构建图标关键字:
f'OUTLINER_OB_{ob.type}'
这个结果可以馈送到循环内的 icon 参数:
col.label(text="Scene Objects:")
grid = col.grid_flow(columns=2)
for ob in context.scene.objects:
grid.label(text=ob.name,
icon=f'OUTLINER_OB_{ob.type}')
我们可以重新加载脚本并查看图标在名称之前是如何显示的:

图 5.18:一个自定义面板,列出场景对象及其图标
我们不希望这个列表在大场景中占用太多空间,因此我们将在一定数量的对象后中断循环。例如,我们可以在列出第四个对象后停止列出对象并显示省略号。
在最后一行留下省略号意味着按行填充网格。为了做到这一点,我们必须将 row_major 参数设置为 True 以用于 grid_flow:
col.label(text="Scene Objects:")
grid = col.grid_flow(columns=2, row_major=True)
for i, ob in enumerate(context.scene.objects):
if i > 3: # stop after the third object
grid.label(text"..")
break
grid.label(text=ob.name,
icon=f'OUTLINER_OB_{ob.type}')
一种(不好的)魔法
代码中间出现的任意数字,如 i > 3 中的那些,被称为魔法数字,使用它们被认为是不良做法,因为它使得在以后阶段找到和更改这些值变得非常困难。
一个更好的解决方案是将这些数字作为类的成员,并在以后访问它们。
将 3 存储为静态成员使其更容易显示剩余对象的数量。字符串格式化也适用于数值变量,因此我们可以计算剩余对象的数量,并将结果用于花括号中:
class OBJECT_PT_very_simple(bpy.types.Panel):
#...
bl_context = 'object'
max_objects = 3
def draw(self, context):
# ...
for i, ob in enumerate(context.scene.objects):
if i > self.max_objects:
objects_left = len(context.scene.objects)
objects_left -= self.max_objects
txt = f"... (more {objects_left} objects"
grid.label(text=txt)
break
由于 max_objects 是类的属性,它可以通过 Python 进行更改。
Blender 将这些插件视为 Python 模块,因此可以在 Python 控制台 或 文本 编辑器区域执行这些行:
import very_simple_panel
very_simple_panel.OBJECT_PT_very_simple.max_objects = 10
这个技巧的缺点是每次重新加载插件都会重置该值。在我们的插件中更改设置的一个更好的方法是使用 bpy.types.Preferences,这将在 第六章 中讨论:

图 5.19:更改限制显示超过三个对象
使用图标和说明性文本增加了我们 UI 的视觉反馈。在下一节中,我们将利用布局状态的颜色来传达状态信息。
提供颜色反馈
如果我们可以突出显示哪些对象被选中以及哪些是活动的,我们的对象列表将更有用。例如,为了在对象名称的颜色中反映选择状态,我们的脚本必须执行两个操作:
-
检查一个对象是否被选中。
-
如果它被选中或处于活跃状态,则以不同的颜色显示其名称。
让我们学习如何使用 Blender 的 API 来完成这些任务。
检查对象是否被选中
我们可以使用对象的select_get()方法来获取对象的选中状态。例如,如果'Cube'对象被选中,其selected_get方法将返回True:
>>> import bpy
>>> bpy.data.objects['Cube'].select_get()
True
我们从第二章**中已经知道,与选择状态不同,活跃并不是对象的标志,因此我们获取这些信息的方式略有不同。
检查对象是否活跃
要检查一个对象是否活跃,我们可以测试它是否与存储在context.object中的对象匹配。以下是当'Cube'是活跃对象时发生的情况:
>>> import bpy
>>> bpy.data.objects['Cube'] == bpy.context.object
True
现在我们已经知道了如何检索对象的活跃状态,接下来让我们看看如何改变其标签的颜色。
用红色或灰色绘制布局
有时候,用不同的颜色绘制文本可以使条目更加突出。Blender 不允许我们显式设置文本的颜色,但我们可以利用两个特定的属性来改变 UI 布局的显示方式:
-
UILayout.enabled = False的目的是在不让用户与之交互的情况下显示一个元素。如果我们想让用户意识到,即使现在无法执行某个操作,执行该操作的界面仍然存在,这将非常有用。 -
UILayout.alert = True对于警告用户有关某些错误或潜在错误的情况非常有用。
这些用途非常具体,但我们可以利用它们对显示颜色的影响:
-
enabled属性等于False的 UI 布局是灰色 -
alert属性等于True的 UI 布局是红色
因此,我们可以利用这一点来改变整个布局的颜色。标签不是布局,label()方法返回None类型。由于我们无法直接在文本标签上设置这些属性,我们需要为网格的每个条目创建一个新的布局,并在创建文本时使用它:
# ...
for i, ob in enumerate(context.scene.objects):
# layout item to set entry color
item_layout = grid.column()
item_layout.label(text=ob.name,
icon=f'OUTLINER_OB_{ob.type}')
我们可以使用这一行将item_layout.enabled设置为True以用于选中的对象,以及False用于未选中的对象:
item_layout.enabled = ob.select_get()
同样,我们可以通过直接赋值等式测试的结果(==)来设置item_layout.alert:
item_layout.alert = ob == context.object
如我们所见,列表现在提供了有关哪些对象是活跃的或选中的信息:

图 5.20:活跃对象为深红色,而未选中的对象为灰色
我们还可以添加按钮来执行一些操作,正如我们将在下一节中看到的。
显示按钮
直观地讲,按按钮执行转换动作。由于按钮占用空间,默认界面只显示更通用的操作。当我们编写自定义界面时,我们可以根据我们的具体需求添加更多按钮。这使得 Blender 将操作符转换为按钮变得更容易。在本节中,我们将学习按钮和操作符在图形界面中的等效性。
使用操作符方法
我们可以使用UILayout.operator方法来显示按钮。在 Blender 中,按钮执行操作符。这个操作符通过其标识符找到——即bl_idname属性,我们在第三章中遇到过——每个操作符都必须有它。
例如,要添加一个删除所选对象的按钮,我们必须提供删除操作符的标识符。
如果我们使用对象菜单中的删除操作或X键,并查看脚本工作区,我们将在信息 日志区域找到这条新行:
bpy.ops.object.delete(use_global=False)
括号前的部分,bpy.ops.object.delete,是操作符类。我们必须小心,因为我们不能将类本身用作操作符的参数,而是该类的标识符。我们可以使用idname()方法来获取标识符:
>>> bpy.ops.object.delete.idname()
'OBJECT_OT_delete'
使用'OBJECT_OT_delete'字符串作为operator()的参数将创建一个删除按钮。
请输入 ID
使用operator类而不是操作符的标识符operator会导致TypeError:操作符及其后面的所有元素将不会显示。
我们可以使用idname()函数或直接使用标识符字符串。函数是首选的,因为它保证了在将来更改时的兼容性。
要显示一个draw函数:
col.operator(bpy.ops.object.delete.idname())
我们将看到以下内容:

图 5.21:已将删除按钮添加到面板中
按下删除按钮将删除所选对象。这相当于从菜单中调用对象 | 删除。
设置操作符的文本和可见性
我们可以自定义按钮文本或切换按钮的显示。例如,我们可以隐藏context:
num_selected = len(context.selected_objects)
我们可以在按钮标签中反映此信息。以下代码片段根据已选对象的数量更改按钮的文本。它还在“object”一词的末尾添加了一个“s”,以便在需要时使用复数形式:
if num_selected > 0:
op_txt = f"Delete {num_selected} object"
if num_selected > 1:
op_txt += "s" # add plural 's'
col.operator(bpy.ops.object.delete.idname(),
text=op_txt)

图 5.22:按钮的文本根据选择而改变
没有要隐藏的内容(通常)
人们常说,隐藏 UI 的一部分通常是错误的,因为它让用户在条件满足后无法知道功能在哪里。这通常是一个有效的观点,尽管为了教学目的,在先前的例子中使用了消失的按钮。
如果我们想遵守“不隐藏”规则,我们可以在 else 语句中添加一个禁用的布局:
if (num_selected > 0):
# …
else:
to_disable = col.column()
to_disable.enabled = False
to_disable.operator(
bpy.ops.object.delete.idname(),
text="Delete Selected"
)
在编码时,规则可以被打破但不能被忽视!
覆盖操作符的设置
delete 操作符在删除对象之前会提示确认对话框。这是它的默认行为,并且可以被覆盖:

图 5.23:点击删除将打开确认菜单
这在文档化的 docstring 中得到了体现。如果我们输入操作符的地址并按下 Tab 键,自动完成将显示两个可选参数,称为 use_global 和 confirm:
>>> bpy.ops.object.delete(
delete()
bpy.ops.object.delete(use_global=False, confirm=True)
Delete selected objects
>>> bpy.ops.object.delete(
您可以通过查看 API 文档来了解更多信息。右键单击 删除 按钮将显示一个包含直接链接的菜单:

图 5.24:我们的删除按钮的右键菜单可以打开在线文档
文档描述了这些布尔参数:
-
use_global(布尔值,可选):全局删除或从所有场景中删除对象 -
confirm(布尔值,可选):确认或提示确认
根据文档,将 use_global 设置为 True 将从所有当前打开的场景中删除选定的对象。我们不想这样做,所以我们没有更改默认值。
另一方面,confirm 参数默认为 True。我们需要将其更改为 False,并且由于按钮负责调用操作符,我们需要在按钮的属性中进行更改。
设置操作符属性
operator 函数返回一个 OperatorProperties 对象,这是一个包含可以设置的属性的类。通常,我们使用以下代码:
col.operator(bpy.ops.object.delete.idname(),
text=op_txt)
相反,我们将存储 operator 返回的属性在 props 变量中,以便我们稍后可以更改它们:
props = col.operator(bpy.ops.object.delete.idname(),
text=op_txt)
props.confirm = False
此按钮触发 delete 操作符,这是 Blender 的原生操作符。由于界面将 Python 和内置操作符视为等效,我们也可以为我们的操作符显示按钮。
为我们的函数添加按钮
我们将为每个选定的对象添加一个随机位移的按钮。这可以用来给我们的场景一个更“自然”的外观。为了做到这一点,我们必须编写一个新的操作符。Blender 的操作符会将所有选定的对象以相同的方式进行变换。首先,我们必须在脚本的开头导入 random 模块:
import bpy
from bpy.utils import previews
import os
import random
我们继续进行位置函数。它可以作为操作符类的一部分,但我们也可以编写一个独立的功能。操作符将在其 execute 方法内部调用该功能。此函数的参数如下:
-
需要错位的对象
-
每个对象位置添加或减去的最大单位数
-
哪个轴应该受到影响
我们将把位移量输入到 randint 函数中,该函数将返回一个介于 min 和 max 范围内的随机整数。我们将为三个轴(X、Y 和 Z)中的每一个都这样做,只要它们的 do_axis 项为 True。amount 和 do_axis 参数是可选的。我们在函数声明中将它们的默认值设置为 1 和 True, True, True:
def add_random_location(objects, amount=1,
do_axis=(True, True, True)):
"""Add units to the locations of given objects"""
for ob in objects:
for i in range(3):
if do_axis[i]:
loc = ob.location
loc[i] += random.randint(-amount, amount)
现在,我们需要一个在界面中显示的操作符。我们将为 amount 和 do_axis 函数参数添加属性。对于操作符,整数和布尔值的元组分别是 IntProperty 和 BoolVectorProperty:
class TRANSFORM_OT_random_location(bpy.types.Operator):
"""Add units to the locations of selected objects"""
bl_idname = "transform.add_random_location"
bl_label = "Add random Location"
amount: bpy.props.IntProperty(name="Amount",
default=1)
axis: bpy.props.BoolVectorProperty(
name="Displace Axis",
default=(True, True, True)
)
操作符方法很简单;poll 只确保已选择对象,而 execute 执行 add_random_location:
@classmethod
def poll(cls, context):
return context.selected_objects
def execute(self, context):
add_random_location(context.selected_objects,
self.amount,
self.axis)
return {'FINISHED'}
将此操作符添加到布局中会显示一个新按钮。如前所述,原生和脚本操作符对界面来说是相同的。在两种情况下,它都会在调用时查找操作符的标识符。尽管如此,脚本操作符提供一个小优势:由于它们的类和我们的图形界面属于同一个模块或包,我们可以直接引用它们的 bl_idname 属性。
这是显示 添加随机 位置 按钮的代码行:
col.operator(
TRANSFORM_OT_random_location.bl_idname
)
当然,我们也不能忽视类注册和删除。以下是应添加到 register() 的代码行:
bpy.utils.register_class(
TRANSFORM_OT_random_location
)
同样,附加组件的 unregister() 函数应包含以下内容:
bpy.utils.unregister_class(
TRANSFORM_OT_random_location
)
在调用 重新加载脚本 后,将出现一个新按钮:

图 5.25:我们的面板现在显示两个按钮
按下此按钮应向所选对象的位置添加随机变化,因为操作符属性在执行时不会弹出。即使添加了我们在 第四章 的 编写电梯插件 部分中了解到的 bl_options = {'REGISTER', 'UNDO'} 操作符属性,也不会改变这一点:当操作符属性不是从 3D 视口 区域运行时,必须显式显示操作符属性。
显示操作符属性
除了 poll 和 execute,Blender 操作符还涉及另一个名为 invoke 的方法。invoke 方法在 execute 之前内部运行。通常我们不需要定义它,但在这个例子中,我们使用它来告诉 Blender 我们想要显示和编辑操作符属性——即我们的函数参数。
除了 self 和 context,invoke 还接受 event 作为参数。它包含有关触发操作符的信息,但我们现在不需要它。我们只告诉 window_manager 显示属性对话框。因此,我们必须在 poll 方法之后添加几行代码:
@classmethod
def poll(cls, context):
return context.selected_objects
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
重新加载脚本并按下 add_random_location 函数:

图 5.26:使用操作符属性作为函数参数
有了这些,我们的对象面板就完成了。作为额外收获,接下来我们将学习如何将其移动到 UI 的不同部分。
使用不同的区域
通常,面板可以自由地移动到界面的另一部分。有一些例外,重新定位面板可能没有太多意义。例如,一个帮助选择角色控制器的工具在视频编辑器中几乎没有帮助,它的poll()方法可能正在寻找动画的context之外的属性,如动画骨骼。
在那些情况下,更改Panel类的bl_*属性就足以将我们的面板移动到不同的位置。请参考本章“创建简单面板”部分中我们查看的面板属性。
因此,为了在bl_space_type和bl_region_type中显示我们的面板,如下所示:
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'

图 5.27:我们的面板已移动到 3D 视口区域
默认情况下,可以使用bl_category属性来指定新标签或现有标签:
class VerySimplePanel(bpy.types.Panel):
"""Creates a Panel in the viewport properties"""
bl_label = "A Very Simple Panel"
bl_idname = "VERYSIMPLE_PT_layout"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Our Panel"
如果 Blender 插件包含多个面板,将它们放在同一个标签下是一种保持界面整洁的好方法:

图 5.28:从 bl_category 属性创建的视口标签
我们已经完成了界面概述的结尾。在介绍列表和缩略图时,下一章将会有更多的 UI 见解,但到目前为止,我们已经对如何在 Blender 中使用布局有了稳固的理解。
摘要
在本章中,我们学习了如何通过 Python 创建和填充自定义UIPanel,以及如何将其集成到我们的插件中。这让我们了解了 Blender 界面的一般工作原理以及我们必须采取哪些步骤来向其中添加小部件。
我们还嵌套了布局以获得更复杂的显示效果,并显示了原生和外部图标。
最后,我们学习了如何根据上下文更改面板的外观,而不会增加太多的复杂性,以及如何向 UI 添加功能。
这本书的第一部分到此结束,我们获得了对 Blender 和 Python 如何协同工作以及 Python 脚本可以做什么的整体理解。
我们编写的插件依赖于一个名为icon_smile_64.png的外部文件。如果我们将其公开分发,我们必须将其打包成 ZIP 文件。这是我们将在第六章中要做的事情,这标志着第二部分,交互式工具和动画的开始。
问题
-
屏幕上的一个区域是否可以容纳多个编辑器?
-
所有编辑器是否都由相同的区域组成?
-
我们如何设置编辑器、区域和上下文,以便面板属于它们?
-
我们是否必须始终设置面板的类别?
-
面板的元素是静态的还是可以动态更改的?
-
我们能否更改文本的颜色?
-
我们如何显示按钮?
第二部分:交互式工具和动画
本部分介绍了与动画系统交互的模块化、结构化附加组件。它还介绍了捕获用户输入和操作员执行不同步骤的模式工具。
本节包括以下章节:
-
第六章,结构化我们的代码和附加组件
-
第七章,动画系统
-
第八章,动画修改器
-
第九章,动画驱动器
-
第十章,高级和模式操作员
第六章:结构化我们的代码和插件
我们迄今为止创建的插件由单个 Python 文件组成。这没问题,但为了处理复杂性,我们可以将我们的代码拆分成同一目录中的相关模块。
编写相互交互的模块而不是单个大文件,使得设计和维护更容易,使我们能够将注意力集中在任务的单一方面。
非代码文件(如图像和其他媒体)的存在可能是采用文件夹结构的另一个原因。这是因为共享一个文件夹比分别处理 Python 脚本和数据更实用。
在本章中,你将学习如何在包的不同模块之间进行编码,并使用导入系统将一切融合在一起。我们将创建的打包插件将更容易分发、阅读和维护,并且只需查看文件名就可以掌握其不同部分的功能。
本章将涵盖以下主题:
-
创建 Python 包
-
加载和刷新代码和资产
-
在你的脚本中读取环境变量
-
使用首选项系统
技术要求
本章我们将使用 Blender 和 Visual Studio Code。本章创建的示例可以在 github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch6 找到。
可选地,在 Windows 上,我们可以使用 7-Zip 创建压缩存档。7-Zip 是一个免费应用程序,可以从 www.7zip.org 下载。
文件夹、包和插件
我们知道插件由 Python 代码和 Blender 插件系统的信息组成。虽然单个文件 Python 脚本被称为模块,但脚本文件夹被称为包。
创建插件文件夹意味着我们将存储 Blender 信息在包级别,因此我们将首先创建一个目录并创建包信息。
创建包文件夹和初始化文件
让我们在我们的 Python 项目中为本章内容创建一个文件夹。然后,在ch6文件夹中作为我们的脚本文件夹并重启 Blender。要创建一个包,我们需要创建一个新的文件夹而不是一个新的文件。我们可以使用文件管理器,或者像以下步骤中那样,使用我们的程序员编辑器的文件栏:
-
选择
PythonScriptingBlender/ch6/addons。 -
通过点击新建文件夹图标创建一个新文件夹:

图 6.1:在 Visual Studio Code 中创建文件夹
- 将新文件夹命名为
structured_addon。
一个 Python 包包含一个名为 __init__.py 的文件。这是文件夹的入口点,Python 在导入包时会自动运行它。要创建它,请按照以下步骤操作:
-
选择
…/``ch6/addons/structured_addon文件夹。 -
通过点击新建文件图标创建一个新文件:

图 6.2:在 Visual Studio Code 中创建新文件
-
将新文件命名为
__init__.py。 -
双击文件打开。
当 Blender 搜索已安装的插件时,它将在该文件夹的 __init__.py 文件中查找 bl_info 字典。我们将以通常的方式填写这些信息。
编写初始化文件
这个 bl_info 字典包含了用于插件发现的常用属性:
bl_info = {
"name": "A Structured Add-on",
"author": "John Doe",
"version": (1, 0),
"blender": (3, 2, 0),
"description": "Add-on consisting of multiple files",
"category": "Learning",
}
提供一个 register() 和一个 unregister() 函数将允许我们启用和禁用插件:
def register():
pass
def unregister():
pass
现在,如果我们重新启动 Blender 或刷新插件列表,我们应该能够看到我们的 结构化插件:

图 6.3:在“学习”类别中列出的结构化插件
我们将通过添加一些内容,使用不同的 .py 文件,使其变得有用。
模块分离的指南
在代码分区和集中之间有一个权衡:过度原子化的代码可能会变得不清晰且难以维护。因此,即使没有关于分割程序的确切规则,也有编写模块化代码的一般标准:
-
非 Python 文件,如 媒体(图像、声音等),根据其类型有自己的子文件夹
-
由无关类使用的 通用代码 可以被视为一个实用模块,并像库一样使用
-
与特定功能相关的 特定代码 应该是特定的模块
-
UI 类,如面板和菜单,可以通过非 UI 代码进行分离
-
可以将 操作符 与非操作符代码分离,并按类别分割
-
仅与脚本的一部分相关的 导入 语句可能意味着整个部分可以放入另一个文件中,从而减少一个文件中导入的库的数量
我们将把这些概念付诸实践,并看看从我们熟悉的例子开始,包架构如何使我们的代码更清晰、更有效:

图 6.4:插件文件夹的执行 – init.py 将所有部分粘合在一起
编写结构化面板
我们在 第五章 中编写了一个用户界面,它依赖于一个外部的 .png 文件来显示图标。这使得工具难以共享,因为 Blender 只能安装一个 .py 或 .zip 文件。
如果我们将所有内容都作为文件夹结构化,我们可以将图片和代码捆绑在一起。根据前面总结的指南,我们可以创建以下内容:
-
一个名为
pictures的 图标存储(媒体文件)的子文件夹 -
一个名为
img_load.py的 图标加载 模块(通用功能) -
一个包含 面板(UI 分离)的模块名为
panel.py -
一个名为
preferences.py的插件 首选项(特定功能)模块 -
一个名为
_refresh_.py的模块,用于重新加载导入系统(维护工具)。
在下一节中,我们将创建一个用于存储图像文件及其加载代码的文件夹。
打包外部图像
如果我们为我们的插件使用图像文件,我们可以在structured_addon目录中创建一个名为pictures的文件夹。由于我们将编写一个加载图标的模块,这个文件夹可以包含一系列图像文件。
在示例中的ch6\addons\structured_addon\pictures文件夹中,我们有pack_64.png,一个表示包的剪贴画,以及smile_64.png,来自上一章的笑脸:

图 6.5:用于此附加组件的图片存储在文件夹中
一旦所有我们的图片都存放在这个文件夹中,我们就可以编写代码来加载它们。
编写图标库
在第五章中,我们编写了一个函数,用于从磁盘加载特定的图像文件。这工作得很好。现在我们正在加载两个图标,我们只需将相同的程序运行两次。
但现在我们有一个用于加载图像的整个模块,我们可以编写一个更复杂的解决方案,它适用于任何数量的图标,因为它不依赖于硬编码的完整路径。
这个新的加载器会扫描图片文件夹中的图像。我们将确保不会因为其他模块的多次调用而增加加载时间,以便我们最终得到一个更灵活、但仍可靠的定制图像加载器。让我们按照之前的步骤创建模块的文件:
-
选择
…/``ch6/addons/structured_addon文件夹。 -
通过点击新建 文件图标创建一个新文件。
-
将新文件命名为
img_loader.py。 -
通过双击打开文件。
此模块将处理整个包的图标加载。
从文件夹中加载图片
img_loader模块会从文件夹中滚动图像文件,因此我们需要导入os包来访问目录。当然,还需要bpy.utils.previews来从文件中加载图像并将它们存储为图标:
from bpy.utils import previews
import os
我们使用一个全局变量,一个列表,来存储 Blender 预览Collection。遵循 Python 命名约定,变量名是大写的,因为它是一个全局变量。此外,它以一个下划线开头,因为它不打算在其他任何模块中使用:
_CUSTOM_ICONS = None
我们创建register_icons()函数来从磁盘加载图标。它类似于第五章第一部分中的load_custom_icons函数。
如果_CUSTOM_ICONS对if条件测试为True,则return语句将立即退出函数。这防止了每次模块使用时重复加载图标。否则,我们通过previews.new()创建一个新的图标集合:
def register_icons():
"""Load icons from the add-on folder"""
global _CUSTOM_ICONS
if _CUSTOM_ICONS: # avoid loading icons twice
return
collection = previews.new()
我们不是通过硬编码文件名来加载的,而是将pictures文件夹中包含的所有图片加载进来。我们不希望加载非图片文件,因此我们将我们可用的图片扩展名存储在img_extensions变量中。在这个例子中,我们只使用.png和.jpg格式,但也可以使用其他格式,例如.tif:
img_extensions = ('.png', '.jpg')
我们通过将'pictures'添加到模块的路径中来获取图片文件夹的路径。在处理文件时,由于os实用工具中的路径函数确保了多平台兼容性,因此我们更倾向于使用路径函数而不是字符串操作。因此,我们使用os.path.join函数构建picture_path变量:
module_path = os.path.dirname(__file__)
picture_path = os.path.join(module_path, 'pictures')
os.listdir函数返回一个包含目录中所有文件名的列表。我们使用for循环遍历列表,并在每次迭代中,使用os.path.splitext函数将文件名与其扩展名分开。扩展名的大小写都是有效的,但字符串比较是区分大小写的操作。我们将所有文件扩展名转换为小写字母,以便.jpg或.png文件可以被考虑。当文件扩展名不在img_extensions中时,文件将通过continue语句被跳过:
for img_file in os.listdir(picture_path):
img_name, ext = os.path.splitext(img_file)
if ext.lower() not in img_extensions:
# skip non image files
continue
快速退出或坚持下去!
使用continue跳出一个循环迭代,使用break跳出整个循环,或者在使用return之前结束一个函数,这些都是当条件不满足时立即中断程序的有效技术。这避免了嵌套过多的if语句,但建议只在执行开始时这样做:在代码中随机位置设置退出点会使代码难以阅读。
os.listdir只列出文件名,而不是它们的完整磁盘路径。要获取它,我们必须使用os.path.join将picture_path和img_file结合起来。我们使用img_name(即没有扩展名的文件名)作为从集合中检索图标的键:
disk_path = os.path.join(picture_path, img_file)
collection.load(img_name, disk_path, 'IMAGE')
一旦for循环结束,我们就可以将集合存储在_CUSTOM_ICONS列表中:
_CUSTOM_ICONS = collection
使用文件名作为关键字很方便,例如'smile_64'将是smile_64.png文件的键,但当我们的文件夹包含具有相同名称但不同扩展名的文件时,例如smile_64.jpg,它可能会产生歧义。我们的脚本将假设图片文件夹不包含具有相同文件名的图片。
有了这个,我们已经创建了register_icons()函数,它初始化图标集合。现在,我们需要添加一个函数来在附加组件被禁用时清理它;否则,我们电脑 RAM 中留下的缩略图将干扰后续执行。
取消注册图标
当附加组件被禁用时,我们必须从我们电脑的内存中卸载其图标。
要做到这一点,我们必须定义unregister_icons函数。在这个函数中,我们调用previews.remove()并使用_CUSTOM_ICONS作为其参数。我们还需要确保在函数末尾将_CUSTOM_ICONS设置为None;否则,Python 将保留对已删除图标的无效引用,导致 Blender 崩溃:
def unregister_icons():
global _CUSTOM_ICONS
if _CUSTOM_ICONS:
previews.remove(_CUSTOM_ICONS)
_CUSTOM_ICONS = None
现在,img_loader可以加载和卸载图标,我们需要的是一个获取器来从其他模块访问_CUSTOM_ICONS。
获取集合
Python 并不允许访问模块成员,即使我们使用带前导下划线的名称将其标记为私有。因此,我们可以通过以下方式访问_CUSTOM_ICONS变量:
>>> img_loader._CUSTOM_ICONS
尽管如此,如果我们使用一个函数来获取已加载的图标,我们还可以添加更多控制:
-
在未来,我们可以将
_CUSTOM_ICONS更改为字典或列表。如果获取它们的函数相应地更改,这将对使用数据的其他模块没有影响。 -
这使得检查条件是否满足变得容易。在这种情况下,我们的
register_icons()调用确保图标已注册,以防万一由于某种原因没有注册。这种做法遵循防御性编程,因为它旨在使脚本即使在不可预见的情况下也能正常工作。 -
这允许我们在某些关键条件未满足的情况下设置障碍。例如,我们添加了一个
assert _CUSTOM_ICONS语句,如果变量尚未设置,即使最近调用了register_icons(),它也会引发错误。这是一个进攻性编程的例子,因为它在出现错误时停止执行:
def get_icons_collection():
"""Get icons loaded from folder"""
register_icons() # load icons from disk
assert _CUSTOM_ICONS # if None something is wrong
return _CUSTOM_ICONS
现在,图片加载器为从图片文件夹中加载、卸载和获取所有图标提供了一个代码接口。主模块可以通过相对导入导入它。
使用相对导入
import语句会在 Python 搜索路径中查找已安装的模块,例如内置库或在我们的情况下,Blender API(bpy)。尝试导入不在搜索路径中的模块会导致脚本因ModuleNotFoundError而停止。
对于属于同一包的模块,我们使用稍微不同的语法,以便访问同一包的其他模块:一个相对导入语句。
在相对导入中,包用点(.)表示,模块使用from . import module语法导入。
因此,在我们的__init__.py文件的导入部分,添加以下内容:
from . import img_loader
注册和注销函数将分别调用img_loader命名空间中包含的register_icons()和unregister_icons()函数:
def register():
img_loader.register_icons()
def unregister():
img_loader.unregister_icons()
现在整个图像加载过程都在一个模块中处理,我们可以编写用户界面的.py文件。
在下一节中,我们将看到,一旦我们通过img_loader的相对导入访问了我们的图标系统,我们就不再需要担心加载图标文件了。
添加用户界面
我们已经创建了 panel.py 模块,该模块将包含所有 用户界面 类和函数,因此这个文件将包含我们的面板类。
编写 UI 模块
我们将通过 img_loader 的相对导入来开始导入 bpy 模块和我们的图标集合:
import bpy
from . import img_loader
OBJECT_PT_structured 类是从 Panel 派生的。像第五章中的那个一样,它在静态部分包含了 Blender 所需的 bl_* 标识符:
class OBJECT_PT_structured(bpy.types.Panel):
"""Creates a Panel in the object context"""
bl_label = "A Modular Panel"
bl_idname = "MODULAR_PT_layout"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'object'
目前,我们的 draw 函数是几行代码,显示一个图标,然后是文本:
def draw(self, context):
layout = self.layout
icons = img_loader.get_icons_collection()
layout.label(text="A Custom Icon",
icon_value=icons['pack_64'].icon_id)
接下来,我们必须编写从该模块注册和注销类的函数:
def register_classes():
bpy.utils.register_class(OBJECT_PT_structured)
def unregister_classes():
bpy.utils.unregister_class(OBJECT_PT_structured)
在模块内部提供注册实用工具,可以减轻 __init__.py 对哪些类在 panel 中定义以及哪些类应该注册/注销的担忧。这就像我们在本章第二部分 获取集合 中所做的,打包 外部图像。
这些设计属于 封装 的实践——也就是说,限制对模块或对象组件的直接访问。遵循它并不本质上更好,但它可以帮助保持代码灵活和整洁。
导入用户界面
在 __init__.py 中,我们从 . 命名空间导入 panel,并调用其 register_classes() 和 unregister_classes() 函数:
from . import img_loader
from . import panel
def register():
img_loader.register_icons()
panel.register_classes()
def unregister():
panel.unregister_classes()
img_loader.unregister_icons()
在这个例子中,unregister 的顺序与 register 函数中的顺序相反。这与插件的执行无关,这里只为了清晰起见。
我们可以通过从 编辑 | 首选项 菜单中启用 结构化面板 来测试我们的代码。
我们可以在对象部分看到面板和我们的新图标:

图 6.6:panel.py 通过 img_loader.py 显示图标
现在,我们将添加其他元素并完成面板。
完成对象面板
在 第五章 中显示的面板的略微简化版本,几乎使用了相同的代码。显示的对象的最大数量仍然存储在 max_objects 静态成员中,但如果将其实现为插件的偏好设置会更好。我们将在本章稍后的 使用插件偏好设置 部分这样做:
class OBJECT_PT_structured(bpy.types.Panel):
"""Creates a Panel in the object context"""
bl_label = "A Modular Panel"
bl_idname = "MODULAR_PT_layout"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'object'
max_objects = 3 # limit displayed list to 3 objects
draw 函数显示场景对象的列表:
def draw(self, context):
layout = self.layout
icons = img_loader.get_icons_collection()
row = layout.row(align=True)
row.label(text="Scene Objects",
icon_value=icons['pack_64'].icon_id)
row.label(text=" ",
icon_value=icons["smile_64"].icon_id)
grid = layout.grid_flow(columns=2,
row_major=True)
for i, ob in enumerate(context.scene.objects):
if i > self.max_objects:
grid.label(text="...")
break
# display object name and type icon
grid.label(text=ob.name,
icon=f'OUTLINER_OB_{ob.type}')
这次,重新加载脚本并没有反映前面代码中的更改:只有 __init__.py 文件在重新加载时被刷新。我们将在下一节中明确介绍重新加载内部模块。
重新加载缓存的模块
当一个模块被导入时,Python 会缓存它的一个副本以供将来访问。由于 __init__.py 文件是唯一一个由 Reload Scripts 操作符更新的文件,我们只剩下两个选项:
-
关闭并重新启动 Blender
-
在
__init__.py中显式调用reload函数
与重新启动应用程序相比,后者更受欢迎,因为它耗时更短。reload 函数是 importlib 模块的一部分。
通过 importlib 进行重新加载
importlib 库中的实用程序与导入系统交互,reload 函数强制 Python 解释器从磁盘重新加载一个模块。
如果 img_loader 模块已更改并需要重新加载,我们可以使用以下命令:
from importlib import reload
reload(img_loader)
因此,为了确保我们的附加功能 .py 文件中的更改始终得到应用,我们可以在 _init_.py 中添加以下代码行:
from . import img_loader
from . import panel
from importlib import reload
reload(img_loader)
reload(panel)
在导入相对模块后不久重新加载它将有效,但将 reload 语句留在已发布的代码中会影响性能,并被视为不良实践。接下来,我们将学习如何在附加功能的特定模块中刷新。
实现刷新模块
调用 reload 会增加附加功能的加载时间,并使代码的可读性降低。许多开发者在工作时添加重新加载调用,完成时移除它们。然而,使用包,我们可以将刷新逻辑移动到另一个模块。
为了做到这一点,我们需要在我们的附加功能包中创建一个文件,并将其命名为 _refresh_.py。此模块不包含任何附加功能,但在我们编辑代码时,它有助于确保所有模块都从磁盘重新加载并保持最新。
重新加载包模块
刷新模块使用了以下元素:
-
importlib模块中的reload函数 -
_refresh_使用的sys模块,用于重新加载其自身的实例 -
bpy模块,用于访问 Blender 首选项 -
附加功能中包含的所有模块
这些要求转化为以下 import 语句:
from importlib import reload
import sys
import bpy
from . import *
通配符字符 * 代表当前包中包含的所有模块(.)。现在,我们可以编写一个函数,如果 Blender 设置为开发模式,则重新加载附加功能模块。我们在 第二章 的 Python 有用功能 部分遇到了 开发者附加功能 设置,在首选项窗口的 界面 选项卡中,可以通过从顶部菜单栏的 编辑 | 首选项 来找到:

图 6.7:在首选项窗口中启用开发者附加功能
我们可以假设当开发者附加功能开启时,我们希望重新加载脚本操作符也重新加载我们的子模块。
使用开发者附加功能作为条件
我们需要找到开发者附加功能的完整 Python 路径以读取其值。为此,请按照以下步骤操作:
-
确保已启用开发者附加功能。
-
此外,请确保已启用用户工具提示和Python 工具提示。
-
将鼠标指针悬停在 开发者附加功能 复选框或标签上。
-
将鼠标停留在原地,不点击或移动它,会显示一个工具提示:
Show options for developers (edit source in context …
Python: PreferencesView.show_developer_ui
根据 API 参考,PreferencesView是Preferences类的view成员,可以在bpy.context.preferences中找到:
docs.blender.org/api/3.3/bpy.types.Preferences.xhtml
bpy.types.Preferences.view
因此,开发者额外功能设置的完整路径如下:
bpy.context.preferences.view.show_developer_ui
show_developer_ui的值要么是True要么是False。使用它作为条件,如果开发者额外功能被禁用,我们将退出reload_modules:
def reload_modules():
if not bpy.context.preferences.view.show_developer_ui:
return
然后,我们为每个我们想要刷新的.py文件添加一个reload调用。第一行重新加载_refresh_.py,在系统字典sys.modules中查找当前文件名。这样,我们可以更新_refresh_模块本身的变化。因此,reload_modules函数的完整体如下所示:
def reload_modules():
if not bpy.context.preferences.view.show_developer_ui:
return
reload(sys.modules[__name__])
reload(img_loader)
reload(panel)
现在,启用__init__.py,因为这是脚本重新加载时唯一要执行的文件。我们必须在导入部分调用_refresh_.reload_modules():
from . import img_loader
from . import panel
from . import _refresh_
_refresh_.reload_modules()
调用OBJECT_PT_structured:

图 6.8:我们的面板源代码已被重新加载
可以在max_objects = 3静态成员中设置三个对象的限制。然而,有一个更好的地方可以存放我们的附加组件设置。在下一节中,我们将实现我们附加组件的正确首选项。
使用附加组件首选项
除了使用 Blender 首选项外,我们还可以使用bpy.types.AddonPreferences在附加组件激活复选框下显示附加组件特定的自定义设置。它是一个接口,就像bpy.types.Panel一样,我们可以使用它的draw方法向其布局添加设置。
AddonPreferences的bl_idname属性必须与附加组件的 Python 名称匹配。对于单个文件使用__name__和对于文件夹使用__package__可以使我们的代码更容易维护:这些变量始终匹配相应的 Python 名称,因此文件和文件夹名称的变化不会产生任何影响。
创建首选项
由于我们使用多个文件,我们将在structured_addon文件夹内创建preferences.py。它包含StructuredPreferences类:
import bpy
class StructuredPreferences(bpy.types.AddonPreferences):
bl_idname = __package__
def draw(self, context):
layout = self.layout
layout.label(text="Structured Add-On Preferences")
然后,我们必须添加一个register_classes和一个unregister_classes函数:
def register_classes():
bpy.utils.register_class(StructuredPreferences)
def unregister_classes():
bpy.utils.unregister_class(StructuredPreferences)
我们可以将preferences添加到__init__.py的import部分,如下所示:
from . import panel
from . import img_loader
from . import preferences
from . import _refresh_
_refresh_.reload_modules()
然后,我们必须将首选项模块中的类与其它类一起注册:
def register():
img_loader.register_icons()
preferences.register_classes()
panel.register_classes()
def unregister():
panel.unregister_classes()
preferences.unregister_classes()
img_loader.unregister_icons()
我们还必须确保首选项将在_refresh_.py中重新加载。_reload_modules函数看起来是这样的:
def _reload_modules():
reload(sys.modules[__name__])
reload(img_loader)
reload(preferences)
reload(panel)
如果我们现在使用重新加载脚本,我们将在附加组件复选框下方看到我们的首选项,在附加组件列表中:

图 6.9:在首选项窗口中显示的附加组件设置
首选项显示出来,但仍然是空的。接下来,我们将添加一些值。
填充首选项
我们想用设置替换 OBJECT_PT_structured .max_objects。它是一个整数,因此我们将向 StructuredPreferences 类添加一个 IntProperty:
import bpy
from bpy.props import IntProperty
class StructuredPreferences(bpy.types.AddonPreferences):
bl_idname = __package__
max_objects: IntProperty(
name="Maximum number of displayed objects",
default=3
)
现在它包含一个整型属性,StructuredPreferences 可以存储最大显示对象设置。为了将此属性显示给用户,我们将在 draw 方法中将它添加到布局中。一个简单的 layout.prop 指令就足够了:
self.layout.prop(self, max_objects)
但我们也可以使用 split 来获得更好的外观。一个分割布局为每个新条目创建一个列。添加一个空小部件,一个 separator 作为第一个元素,创建一个缩进:
def draw(self, context):
layout = self.layout
split = layout.split(factor=0.5)
split.separator()
split.label(text="Max Objects")
split.prop(self, 'max_objects', text="")
重新加载脚本将显示 最大对象数 作为可编辑设置:

图 6.10:最大对象数作为首选项设置
此值将与其他用户首选项一起保存,并且 Blender 在应用程序重新启动时记住其值。我们目前还没有使用此设置:我们需要调整我们的面板中的代码,以便可以使用它。
在代码中使用扩展首选项
Python 脚本可以通过一行代码访问扩展的首选项:
bpy.context.preferences.addons[ADDON_NAME].preferences
注意 preferences 在末尾被重复。这可能看起来是多余的,但这是有意义的,因为 bpy.context.preferences.addons 指的是应用程序首选项,而不是单个扩展的首选项。
bpy.context.preferences.addons[ADDON_NAME] 返回扩展作为 Python 对象。
在这个前提下,我们将回到 OBJECT_PT_structured 类在 用户界面 的 panel.py 模块中的 OBJECT_PT_structured 类。由于我们将使用首选项中的值,它不再应该有一个 max_objects 静态成员:
class OBJECT_PT_structured(bpy.types.Panel):
"""Creates a Panel in the object context"""
bl_label = "A Modular Panel"
bl_idname = "MODULAR_PT_layout"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'object'
现在,在我们迭代 draw 函数中的场景对象之前,我们必须从上下文中获取我们的 add_on 和 preferences。使用 enumerate,这样我们可以在显示对象时保持计数,当 preferences.max_objects 中存储的数量达到时,我们停止循环:
add_on = context.preferences.addons[__package__]
preferences = add_on.preferences
for i, ob in enumerate(context.scene.objects):
if i >= preferences.max_objects:
grid.label(text="...")
break
这次,我们使用大于或等于 (>=) 的比较来检查 max_objects,因为枚举从 0 开始,在 i > max_objects 后中断会显示一个额外的对象。
为了清楚起见,使用单独的模块为扩展首选项不是必需的——本章中编写的全部代码本可以包含在一个单独的大 .py 文件中:我们只是为了可读性而拆分代码。
如果我们的扩展包含操作符,我们也可以为它们创建模块。
添加操作符
操作符可以根据其目的分组到不同的文件中。例如,与转换相关的操作符,如 ops_transform.py,而我们的前几个操作符,ops_collections.py 文件。所有这些类随后将通过 __init__.py 进行注册,如果需要,可以通过相对导入添加到扩展接口中。
另一个解决方案是为所有操作符创建一个模块,可以命名为 operators.py。在本节中,我们将为我们的扩展创建一个操作符模块。
编写操作员模块
在structured _addon文件夹中,我们将创建operators.py模块。它将包含我们的操作员类:我们将重用 Blender 脚本中无处不在的bpy,我们将导入random模块并在add_random_location函数中使用randint:
import bpy
import random
def add_random_location(objects, amount=1,
do_axis=(True, True, True)):
"""Add units to the locations of given objects"""
for ob in objects:
for i in range(3):
if do_axis[i]:
loc = ob.location
loc[i] += random.randint(-amount, amount)
现在,我们可以继续添加附加类。它和前一章的显示按钮部分相同 - poll返回True表示有选中的对象,而execute运行add_random_location,以操作员的数量和轴作为参数:
class TRANSFORM_OT_random_location(bpy.types.Operator):
"""Add units to the locations of selected objects"""
bl_idname = "transform.add_random_location"
bl_label = "Add random Location"
amount: bpy.props.IntProperty(name="Amount",
default=1)
axis: bpy.props.BoolVectorProperty(
name="Displace Axis",
default=(True, True, True)
)
@classmethod
def poll(cls, context):
return context.selected_objects
def execute(self, context):
add_random_location(context.selected_objects,
self.amount,
self.axis)
return {'FINISHED'}
就像在panel.py的情况一样,我们必须添加注册模块类的函数:
def register_classes():
bpy.utils.register_class(TRANSFORM_OT_random_location)
def unregister_classes():
bpy.utils.unregister_class(
TRANSFORM_OT_random_location
)
现在,我们可以将operators.py导入脚本的其他模块。
注册操作员类
要使用我们的操作员,我们必须在__init__.py中导入operators.py,其import部分将如下所示:
from . import operators
from . import img_load
from . import panel
from . import preferences
from . import _refresh_
当然,我们可以使用operator.register_class和operator.unregister_class添加和删除我们的操作员。由于操作员可能用作按钮,我们在panel.register_classes之前调用operators.register_classes:
def register():
preferences.unregister_classes()
operators.register_classes()
img_load.register_icons()
panel.register_classes()
def unregister():
panel.unregister_classes()
img_load.unregister_icons()
operators.register_classes()
preferences.unregister_classes()
这样,当我们在重新加载脚本时,operators.py将生效,我们必须将operators添加到_refresh_.reload_modules中。
重新加载时刷新操作员
多亏了我们在重新加载缓存模块部分所做的努力,向刷新的模块添加操作员变得容易:我们在reload_modules函数中添加reload(operators)。现在整个_refresh_.py文件如下所示:
import sys
from importlib import reload
import bpy
from . import *
def reload_modules():
if not bpy.context.preferences.view.show_developer_ui:
return
reload(sys.modules[__name__])
reload(img_load)
reload(preferences)
reload(operators)
reload(panel)
剩下的唯一事情就是在面板中显示操作员按钮。
添加操作员按钮
要添加panel.py,其导入部分将如下所示:
import bpy
from . import img_loader
from . import operators
现在,我们的面板可以使用operators作为命名空间访问TRANSFORM_OT_random_location类,因此我们将向draw方法添加一个新元素:
layout.operator(
operators.TRANSFORM_OT_random_location.bl_idname
)
当显示F3 搜索栏区域查找并运行重新加载脚本时,我们的面板将显示添加随机 位置按钮:

图 6.11:通过相对导入显示操作员的模块化面板
我们的附加组件完成了。然而,我们可以改进panel.py并添加我们在第五章中为VerySimplePanel编写的相同功能,即以下内容:
-
为选定的/活动对象着色的条目
-
一个具有上下文相关标签的删除按钮
实现这些留作练习。在下一节中,我们将学习如何将我们的附加组件文件夹分发给其他用户。
打包和安装附加组件
我们在第三章的安装我们的附加组件部分学习了如何安装单个.py附加组件。要分发由多个文件组成的附加组件,我们必须创建一个包含它的.zip存档。你们大多数人都会熟悉.zip文件是一个压缩存档,可以包含更多文件或文件夹。
Blender 可以从标准的 .zip 归档中安装文件夹,但有两条要求:
-
.zip文件必须包含插件作为一级文件夹 -
第一级文件夹的名称不能包含任何点 (
.),因为它不会与 Python 的导入系统兼容。
有第三方工具,例如使用操作系统的文件实用程序创建 .zip 文件。在本节中,我们将学习如何在 Windows、OSX 和 Ubuntu 系统上压缩插件文件夹。
清理字节码
如果 structured_addon.zip\structured_addon 文件夹包含名为 __pycache__ 的子文件夹,请确保您删除它:您不应该分发它包含的 .pyc 文件。
使用 7-Zip 创建 .zip 文件
7-Zip 是一款适用于 Windows 的免费压缩工具。它非常轻量级,并且与文件管理器集成。以下是使用它打包我们的插件步骤:
-
从
www.7-zip.org下载并安装 7-Zip。 -
打开 文件资源管理器。
-
导航到包含
structured_addon文件夹的目录。 -
右键单击
structured_addon文件夹以显示上下文菜单。 -
选择 7-Zip | 添加 到“structured_addon.zip”。
structured_addon.zip 文件将与原始文件夹一起创建。如果由于某种原因无法安装 7-Zip 或任何其他压缩工具,我们仍然可以使用 Windows 文件资源管理器单独创建 .zip 文件。
使用 Windows 文件管理器创建 .zip 文件
-
打开 文件资源管理器。
-
导航到我们想要创建插件的文件夹。
-
通过右键单击背景来显示上下文菜单。
-
从右键菜单中选择
structured_addon.zip:

图 6.12:使用 Windows 文件资源管理器创建空 .zip 文件
到目前为止,我们已经创建了一个 .zip 文件,但它仍然是空的。接下来,我们将把我们的插件文件复制到那里:
-
通过使用 Ctrl + C 或右键单击并选择 复制,将
ch6\addons\structured_addon文件夹复制到剪贴板。 -
双击
structured_addon.zip归档以显示其内容。 -
通过 Ctrl + V 或右键单击并选择 粘贴,将
ch6\addons\structured_addon文件夹粘贴到归档中。
在 Mac 上使用 Finder 创建 .zip 文件
按照以下步骤操作:
-
在 Finder 中右键单击
structure_addon文件夹或左键单击不释放按钮。将出现一个菜单。 -
从菜单中选择 压缩“structured_addon”:

图 6.13:在 Mac 计算机上压缩文件夹
使用 Gnome 创建 .zip 文件
使用 Gnome 创建 .zip 文件:
-
在文件浏览器中右键单击
structure_addon文件夹。 -
从菜单中选择 压缩… 选项。
-
确认归档的文件名和
.zip扩展名。 -
点击 创建 按钮。
安装 .zip 插件
安装压缩附加组件的步骤与我们在第三章的通过创建简单的附加组件扩展 Blender部分中学到的步骤相同:
-
通过顶部的菜单编辑 | 首选项打开首选项窗口。
-
在左侧列中选择附加组件标签页。
-
点击附加组件首选项右上角的安装按钮。
-
在文件浏览器中找到
structured_addon.zip文件。 -
点击底部的安装附加组件按钮。
有了这些,我们已经完成了我们结构化附加组件的完整开发和发布。在编程中,模块化方法很重要,并且适用于相对简单的工具。此外,随着复杂性的增加,它使我们的脚本更容易管理。
摘要
在本章中,我们学习了如何通过将代码拆分为不同的文件来设置代码的模块化架构,同时保持其连贯性和清晰性。我们还学习了如何按程序加载文件以及如何为我们的附加组件编写设置。
这种方法为我们的代码提供了互操作性和通用性,并且通过应用适当的分离指南,简化了我们导航工具不同部分的任务。
在第七章中,我们将学习如何使用 Python 进行动画制作,并使用我们的脚本来更改动画设置。
问题
-
Blender 附加组件可以由多个文件组成吗?
-
附加组件文件夹中的哪个文件包含信息字典?
-
相对导入是如何工作的?
-
重新加载附加组件是否会刷新所有模块?
-
我们在哪里存储附加组件的设置?
-
我们如何在首选项中显示附加组件属性?
-
我们如何分发多文件附加组件?
第七章:动画系统
3D 的流行很大程度上归功于动画内容的制作。它在性能、质量和可扩展性方面的许多优势使其在电影、卡通、特技动画和视频游戏中无处不在。这随之而来的是需要定制工具来简化与动画相关的任务。
大多数应用程序以类似的方式处理动画,部分原因是从手绘工作流程中继承而来:一个序列被分解成帧,这些帧的快速连续出现产生了运动的错觉。
在 3D 领域工作的程序员必须考虑到动画值随时间变化,以及这些数据是如何存储的。
这可能在将来改变,但在撰写本文时,动画涉及大量的手动工作,为自动化留下了很大的空间。
在本章中,你将熟悉 Blender 的动画过程,学习如何在 Python 中访问动画数据,并构建一个设置播放范围的工具以及一个使对象动画化的工具。
本章将涵盖以下主题:
-
理解动画系统
-
更改播放设置
-
使用 Python 创建动画
技术要求
在本章中,我们将使用 Blender 和 Visual Studio Code。本章创建的示例可以在github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch7找到。
理解动画系统
虽然动画由一系列帧组成,但屏幕上一次只显示一帧。动画师可以滚动这些帧,像播放视频一样播放他们的动画:

图 7.1:在 Blender 中播放动画
在布局工作空间屏幕底部,动画时间轴控制并显示当前帧以及序列的开始/结束。它提供即时视觉反馈,对于动画至关重要。
时间轴和当前帧
时间轴是 Blender 中播放动画和更改播放设置的区域。它带有时钟图标,由于其重要性,它存在于多个工作空间中:动画、布局、渲染和合成都显示时间轴。
除了帧的开始和结束值之外,还有一个用于当前帧区域的滑块和一个带有媒体控制按钮栏:

图 7.2:Blender 的动画时间轴
除了帧范围和控制之外,时间轴还显示关键帧——即包含对象属性变化的帧。
帧范围信息,这对于其他任务(如渲染)也相关,是场景的一个属性,也可以在场景属性中设置。
持续时间和帧率
当前场景的时长可以在输出属性的格式和帧范围面板中设置,从上面的第二个标签开始,带有打印机图标。
一个场景,四个标签页!
场景属性如此之多,以至于它们跨越了前四个标签页。这可能会令人困惑,因为它们的所有标题都读作场景。
这四个类别如下:
-
渲染,带有电视图标
-
输出,带有打印机图标
-
视图层,其图标是一堆图片
-
场景,其图标代表一个圆锥体和一个球体
只有输出属性包含与动画相关的设置。
帧率属性显示动画每秒包含多少帧,也称为每秒帧数(FPS)。
默认值是24 FPS,这是历史上有声电影的速率。由于电网的频率,美国电视的素材通常以30 FPS 拍摄,而欧洲电视使用25 FPS。动作捕捉或视频游戏动画可能有更高的速率,例如60 FPS:

图 7.3:场景范围属性
提高帧率值会使动画每秒播放更多帧,因此缩短了播放时长,因为更多的帧在更少的时间内完成。
虽然图形界面允许动画师从预设列表中选择帧率或显示自定义值的附加属性,但在 Python 中,fps是scene.render属性的一个数值属性:
>>> import bpy
>>> bpy.context.scene.render.fps
24
默认情况下,场景的第一帧是1,而最后一帧是250。这些值是任意的,并且会根据特定镜头的计划时长进行更改。
场景的第一帧和最后一帧可以通过 Python 作为场景属性访问:
>>> import bpy
>>> bpy.context.scene.frame_start
1
>>> bpy.context.scene.frame_end
250
1。它可以增加,以便不是所有帧都被渲染,这是渲染管理器用来测试序列整体状态的功能:
>>> import bpy
>>> bpy.context.scene.frame_step
1
虽然序列的开始、结束和步长在输出属性中也是可用的,但当前帧和预览范围仅在时间轴视图中显示。
当前帧和预览范围
与frame_start、frame_end和frame_step一样,当前帧作为场景的 Python 属性公开:
>>> import bpy
>>> bpy.context.scene.frame_current
1
在时间轴视图中,位于开始/结束左侧带有计时器图标按钮切换预览范围。它显示一个不同的范围,与渲染设置不同:

图 7.4:在时间轴视图中启用预览范围
预览范围之外的区域用深橙色标记。动画师在处理分配序列的一部分时,会打开预览范围以限制播放。
在 Python 中,我们可以这样访问frame_preview_start和frame_preview_end:
>>> import bpy
>>> bpy.context.scene.frame_preview_start
1
>>> bpy.context.scene.frame_preview_end
250
在start和end之间的所有帧构成了动画序列,但并非所有帧都必须存储信息。那些存储信息的帧是动画的关键帧。
动画关键帧
在某一时刻物体的位置被存储为关键帧。在本节中,我们将简要介绍如何在用户界面以及使用 Python 指令创建和检索关键帧。
在 Blender 中添加关键帧
在 Blender 中设置关键帧至少有两种方式:
-
右键单击一个属性,然后在上下文菜单中点击插入关键帧(s)。
-
在视图中按I显示插入关键帧菜单区域并选择要动画化的属性
我们可以在不同的时间进行更改和插入关键帧来创建动画。
动画对象
为了更熟悉动画,我们可以打开 Blender 并为默认立方体的位置添加关键帧:
-
打开 Blender 并选择一个对象。如果场景中没有对象,我们可以从顶部菜单使用添加 | 网格 | 立方体。
-
按N键显示活动对象的属性。
-
右键单击任何位置属性。
-
选择插入关键帧。
然而,一个关键帧不足以让立方体在屏幕上移动。要创建合适的动画,我们需要做以下事情:
-
为
24设置一个新值。 -
按下G键并将鼠标滚轮移动到新位置以移动立方体。
-
通过左键单击或按Enter键确认新位置。
-
按
I插入一个位置关键帧。
我们可以通过按空格键或点击播放按钮来播放我们的动画。在某些情况下,我们可能想要限制帧范围以循环观看我们的动画。我们可以手动设置序列的开始和结束,或者我们可以编写一个插件为我们设置它们。
编写动作到范围插件
动画师根据镜头的持续时间设置场景的第一帧和最后一帧。如果有动画对象,此插件可以自动设置播放范围。
此操作符将允许您在渲染和预览范围之间进行选择。
设置环境
让我们在项目中为这一章节创建一个文件夹。然后,在ch7文件夹中作为我们的脚本文件夹。我们必须重新启动 Blender 以更新其搜索路径。
我们的插件包含一个操作符,就像第三章和第四章中的那些:
-
选择
PythonScriptingBlender/ch7/addons。 -
通过点击新建 文件图标创建一个新文件。
-
将新文件命名为
action_to_range.py。 -
双击文件以打开它。
我们现在可以开始编写我们的第一个动画插件。
编写动作到范围信息
操作符将从时间轴视图的视图菜单中调用,如位置信息中所述:
bl_info = {
"name": "Action to Range",
"author": "John Packt",
"version": (1, 0),
"blender": (3, 00, 0),
"location": "Timeline > View > Action to Scene Range"
"description": " Action Duration to Scene Range",
"category": "Learning",
}
下一步操作如下:
-
编写操作符。
-
编写其菜单条目。
-
注册类和用户界面。
让我们从操作符类及其信息开始。
编写动作到范围操作符
如同往常,ActionToSceneRange操作符从bpy.types.Operator派生,并以bl_*标识符开始:
import bpy
class ActionToSceneRange(bpy.types.Operator):
"""Set Playback range to current action Start/End"""
bl_idname = "anim.action_to_range"
bl_label = "Action Range to Scene"
bl_description = "Transfer action range to scene range"
bl_options = {'REGISTER', 'UNDO'}
如同在理解动画系统部分中提到的,场景中有两个帧范围设置:主范围影响场景渲染,而预览范围仅影响视口播放。
我们需要一个参数在两个之间切换。我们将使用BooleanProperty,这样我们就可以影响主范围或预览范围:
use_preview: bpy.props.BoolProperty(default=False)
这就是操作符静态部分的全部内容。添加poll和execute方法将允许操作符运行。
编写操作符方法
正如我们在第三章中学到的,当操作符运行的条件不满足时,poll方法返回False;否则返回True。我们需要确定操作符的要求并将它们以 Python 形式表示。
在poll()方法中检查要求
为了获取活动动画的范围,我们必须验证以下条件:
-
必须有一个活动对象
-
活动对象必须是动画化的
当一个对象被动画化时,其关键帧被分组到一个动作中,这个动作反过来又成为对象动画数据中的活动动作。
我们将在下一节更深入地探讨这些实体,在 Python 中访问动画数据。现在,我们可以在以下代码中测试它们的存在:
@classmethod
def poll(cls, context):
obj = context.object
if not obj:
return False
if not obj.animation_data:
return False
if not obj.animation_data.action:
return False
return True
当任何not条件满足时,操作符在界面中变灰。否则,可以启动操作符,这将运行其execute方法。
编写execute方法
execute方法执行操作符活动。它执行以下操作:
-
查找当前动作的帧范围
-
根据场景相应地设置第一帧和最后一帧
-
在时间轴上可视地适应新的帧范围
我们已经知道如何访问活动对象的动作。它的frame_range属性包含动作的第一帧和最后一帧:
def execute(self, context):
anim_data = context.object.animation_data
first, last = anim_data.action.frame_range
我们获取当前scene并执行步骤 2。如果使用时间轴预览范围,我们应该设置预览的开始/结束帧。帧值存储为十进制浮点数,在使用它们之前我们需要将它们转换为整数:
scn = context.scene
if self.use_preview:
scn.frame_preview_start = int(first)
scn.frame_preview_end = int(last)
否则,我们必须设置场景的标准frame_start和frame_end:
else:
scn.frame_start = int(first)
scn.frame_end = int(last)
现在我们已经设置了我们的值,我们可以调用ops.action.view_all()来使时间轴视图适应新的范围,并完成执行:
bpy.ops.action.view_all()
return {'FINISHED'}
我们需要将'UNDO'添加到bl_options中,以便在执行后影响操作符属性,正如我们在第四章中学到的:
bl_options = {'REGISTER', 'UNDO'}
不幸的是,在use_preview设置为False之外启动的操作符。
为了解决这个问题,我们可以在时间轴 | 视图菜单中创建两个条目。
编写菜单函数
在第三章中,我们了解到向菜单类添加一个函数可以使我们向其布局中添加元素。
此外,在第五章中,我们了解到操作符属性是由layout.operator函数返回的,并且可以编程设置。
结合这两种技术,我们可以创建两个菜单条目,它们调用相同的操作符,但我们只在一个条目上启用use_preview。这样,我们最终有两个菜单项。它们执行相同的操作符,但具有不同的设置和结果。
为了确保差异反映在操作符标签上,我们可以使用text=参数来更改它:
def view_menu_items(self, context):
props = self.layout.operator(
ActionToSceneRange.bl_idname,
text=ActionToSceneRange.bl_label +
" (preview)")
props.use_preview = True
我们已经将" (preview)"添加到操作符标签中,以便清楚地表明这是一个ActionToSceneRange操作符的变体。
默认条目影响实际场景范围,因此不需要显式指定文本标签:
props = self.layout.operator(
ActionToSceneRange.bl_idname
)
props.use_preview = False
菜单条目按照后进先出(LIFO)策略显示。我们首先添加了(preview)项,所以它将在默认的动作到场景 范围条目之后显示。
它不会永远默认!
ActionToSceneRange.use_preview默认为False,但在view_menu_items中我们仍然将props.use_preview设置为False。
除非is_skip_save用于一个属性,否则默认值只影响操作符的第一个执行。从那时起,最后的用户选择成为新的默认值。
如果界面元素与特定的操作符设置匹配,那么我们应该在代码中显式设置它们。
现在我们已经创建了附加组件的元素,我们需要注册操作符和菜单条目。然后,它就可以使用了。
完成附加组件
我们需要时间轴 | 查看菜单的类名。为了找到它,我们可以查找 Blender 界面的 Python 源代码。
查找菜单的 Python 类
我们可以右键点击时间轴 | 查看菜单,选择编辑源来找到它的 Python 名称:

图 7.5:打开时间轴 | 查看
然后,在文本 编辑器区域的space_time.py中:

图 7.6:space_time.py 作为一个加载的文本块
在menu()函数中找到的脚本就是我们正在寻找的类名:
sub.menu("TIME_MT_view")
我们可以在我们的register()函数中使用这个名称。
编写注册/注销函数
我们可以使用register_class将ActionToSceneRange添加到 Blender 操作符中,并将我们的条目追加到TIME_MT_view以在时间轴 | 查看菜单中显示我们的新条目:
def register():
bpy.utils.register_class(ActionToSceneRange)
bpy.types.TIME_MT_view.append(view_menu_items)
同样,当附加组件被禁用时,unregister()会从菜单中移除我们的条目,并从 Blender 中移除操作符:
def unregister():
bpy.types.TIME_MT_view.remove(view_menu_items)
bpy.utils.unregister_class(ActionToSceneRange)
现在,操作符已经准备好了。我们可以用它来操作本章前面动画过的立方体,或者打开ch7/_scenes_中包含的其中一个文件。
启用和运行
如果PythonScriptingBlender/ch7文件夹被添加到scripts路径中,我们可以在插件首选项中找到并启用动作到范围:

图 7.7:启用动作到范围插件
如果插件安装正确,我们将在时间线 | 视图中找到两个新的条目:

图 7.8:动作到场景范围及其“预览”变体
点击动作到场景范围将场景范围设置为 1-24,而动作到场景范围(预览)则设置预览范围。
由于我们在属性中设置了bl_options = {'REGISTER', 'UNDO'},我们将看看这个操作符如何支持调整最后 操作窗口。
更改最后操作的参数
我们可以使用顶部的菜单栏中的编辑 | 调整最后操作来更改上一次执行中的选项:

图 7.9:更改最后操作的结果
将出现一个小窗口,显示操作符属性。打开和关闭use_preview会改变操作的结果:

图 7.10:动作到场景范围属性窗口
插件已完成,但当涉及到重新居中bpy.ops.action.view_all(),一个时间线操作符时。在execute中调用其他操作符是可以的,但它们可能会对上下文的合法性施加额外的限制,因此我们必须考虑它们的poll方法可能会停止我们的脚本执行。
例如,通过将我们的操作符添加到需要它的action.view_all()中,永远不会失败。
但如果出现F3 RuntimeError:

图 7.11:如果时间线未显示,我们的脚本将引发错误
我们可以只是警告用户或在我们的poll方法中检查时间线的存在,但通常,最佳实践如下:
-
在调用其他操作符时使用
try语句 -
如果可能,如果发生
RuntimeError,为运行其他操作符创建一个替代的context。
这样,即使出了问题,我们的操作符也会继续执行其任务。
为其他操作符修复上下文
通过使用try和catch语句,我们可以防止 Python 脚本在出现错误时停止。可能引发错误的代码位于try缩进块下,而特定错误发生时执行的代码位于except ErrorType缩进下。
在我们的案例中,当RuntimeError发生时,错误信息被触发:
RuntimeError: Operator bpy.ops.action.view_all.poll() failed, context is incorrect
为了解决这个问题,我们必须在except RuntimeError块中提供一个备用计划。如果我们不想做任何事情,我们可以使用空的pass指令:
try:
bpy.ops.action.view_all()
except RuntimeError:
pass
但我们可以做得更好:我们可以在窗口中查找时间轴,覆盖context,并将其传递给操作符。
在第五章中,我们了解到 Blender 窗口被分为screen、areas和regions。从文档中,我们知道时间轴编辑器属于'DOPESHEET_EDITOR'类型。
可能会有更多窗口打开。对于每一个,我们可以获取屏幕属性:
for window in context.window_manager.windows:
screen = window.screen
然后,我们必须在屏幕的areas中查找'DOPESHEET_EDITOR':
for area in screen.areas:
if area.type != 'DOPESHEET_EDITOR':
continue
通过跳过每个不是'DOPESHEET_EDITOR'的区域,我们可以确保以下行只在区域是时间轴时执行。我们需要查找其主区域,它属于'WINDOW'类型:
for region in area.regions:
if region.type == 'WINDOW':
时间轴的window、area和region在with语句中传递到context.temp_override。
在 Python 中,with设置一个在它的作用域内保持有效的条件——即缩进的代码行。在那里,我们可以调用bpy.ops.action.view_all():
with context.temp_override(
window=window,
area=area,
region=region):
bpy.ops.action.view_all()
break
break
return {'FINISHED'}
两个break语句在找到时间轴后停止搜索。我们确保只有在满足条件时才调用view_all。
我们通过检查动作帧范围来自动化一项繁琐的操作,而不需要查看它包含的关键帧。为了了解我们如何访问和操作动画数据,接下来,我们将学习如何显示和编辑关键帧。
编辑关键帧
动画软件通过视觉提示关键帧分布。在 Blender 中,关键帧在界面中以特殊颜色显示,并在动画编辑器中以菱形小部件显示。
动画属性有彩色背景。如果当前帧是关键帧,则背景为黄色;否则,为绿色:

图 7.12:位置被动画化;当前帧是 X 和 Y 的关键帧
选择对象的关键帧在时间轴编辑器中以菱形显示:

图 7.13:动画时间轴。帧 1 和 24 有关键帧
Blender 通过在两个关键帧之间绘制图表来从一个关键帧过渡到另一个。这些图表被称为动画曲线或f 曲线。
动画曲线和图编辑器
与大多数动画软件一样,Blender 通过在两个动画值之间插入两个或更多关键帧来生成过渡。关键帧包含两个元素——一个时间点和一个属性在该时间点的值。
这些随时间变化的价值在图编辑器区域中表示,这是一个坐标系统,其中水平轴是帧号,垂直轴是每帧的动画值:

图 7.14:在图编辑器中的时间值作为动画曲线
在帧1创建的将属性设置为0的关键帧显示为一个坐标为(``1, 0)的点。
Blender 在关键帧之间进行插值。关键帧及其邻居之间的过渡是一个F 曲线——即连接两个关键帧的平滑连续图。
说他的 F 名!
F 曲线是以波音公司的研究员詹姆斯·弗格森的名字命名的,他在 1964 年发表了一篇名为多变量曲线插值的论文。他的插值公式推动了现代计算机图形学的发展。
这样,每个动画曲线,或F 曲线,都包含动画师设置的关键帧和 Blender 生成的过渡,既作为动画数据的存储,也作为填充它们缺失部分的插值器。
插值可以使用连接点的直线,或者带有切线手柄的曲线线——即贝塞尔曲线。
设置位置关键帧会为X、Y和Z通道创建曲线。
动画曲线在图形编辑器区域中显示。我们可以从任何区域标题左侧的下拉列表中选择图形编辑器:

图 7.15:将图形编辑器作为 Blender 区域的内容
动画的 f 曲线存储在动作中,它们是动画数据中的顶级容器。
动画数据结构可以总结为动作 | F 曲线 | 关键帧。
通过 Python 遍历这个层次结构的方式不同,我们可以从我们的脚本中检索动画值。
在 Python 中访问动画数据
让我们切换到脚本工作区区域,以便熟悉动画系统 API。
在 Python 中添加关键帧
每个可动画对象的 Python 类都提供了一个我们可以用来插入关键帧的方法,名为keyframe_insert。它与指定要动画化的属性的data_path字符串非常相似。可选参数如index和frame允许我们指定一个聚合属性的某个通道或不同于当前帧的帧:
keyframe_insert(data_path,
index=- 1,
frame=bpy.context.scene.frame_current,
[…]
Returns
Success of keyframe insertion.
以下行在帧1为活动物体的位置设置了10.0, 10.0, 10.0的关键帧:
>>> import bpy
>>> bpy.context.object.location = 10.0, 10.0, 10.0
>>> bpy.context.object.keyframe_insert('location', frame=1)
True
动画需要随时间变化的值,所以一个关键帧是不够的。我们将在帧 24 设置另一个值:
>>> bpy.context.object.location = -10.0, -10.0, -10.0
>>> bpy.context.object.keyframe_insert('location',frame=24)
True
我们只在动画的开始和结束处设置了一个关键帧,但默认情况下,Blender 会在两个相邻关键帧之间生成一个过渡,使得物体在 1 到 24 帧之间的每一帧都会稍微移动一点。
我们的对象从-10.0, -10.0, -10.0的10.0, 10.0, 10.0坐标开始。
从几何学角度来说,这些坐标标记了立方体的前上角和底左角,意味着运动发生在三维空间的对角线上。
在 Python 中检索关键帧
如果活动对象有关键帧,我们可以遍历其 animation_data:
>>> bpy.context.object.animation_data
bpy.data.objects['Cube']...AnimData
由于 animation_data 包含当前动作、所有 f-curves 和关键帧,我们将大量使用此容器。将其存储为变量可能很方便,这样我们可以在收集数据时避免长代码行。以下是获取当前 action 的方法:
>>> anim_data = bpy.context.object.animation_data
>>> anim_data.action
bpy.data.actions['CubeAction']
从动作中,我们可以检索到动画 fcurves 列表:
>>> action = anim_data.action
>>> anim_data.action.fcurves
bpy.data.actions['CubeAction'].fcurves
对于每个曲线,我们可以获取动画的 data_path。数据路径标识了在 Blender 中属性存储的位置,但某些属性,如 location,需要每个通道的动画曲线 – 例如,一个用于 x,一个用于 y,一个用于 z 坐标。因此,f-curves 也具有 array_index 属性,一个指定聚合属性动画通道的数字。如果我们动画 location 的三个通道并使用 Python 滚动 f-curves,我们将找到三个具有相同路径 'location' 的曲线,每个曲线具有不同的索引:
>>> fcurves = anim_data.action.fcurves
>>> for fc in fcurves:
... print(fc.data_path, fc.array_index)
...
location 0
location 1
location 2
每个 keyframe_point 在 co 属性中存储两个坐标。第一个是帧号,而第二个是该帧的值:
>>> for fc in fcurves:
... print(fc.data_path, fc.array_index)
... for kf in fc.keyframe_points:
... frame, value = kf.co
... print("\t frame", frame, "value", value)
location 0
frame 1.0 value 0.0
frame 24.0 value 0.2
location 1
frame 1.0 value 0.0
frame 24.0 value 4.0
location 2
frame 1.0 value 0.0
frame 24.0 value 3.0
当前的、第一个和最后一个场景帧存储为整数,而 co[0] 是一个 float。这允许我们在相邻帧之间插入动画(子帧动画)。
曲线插值模式存储在关键帧的 interpolation 属性中。最常用的插值如下:
-
'CONSTANT': 无插值 -
'LINEAR': 使用直线插值 -
'BEZIER': 使用带 handles 的曲线插值
贝塞尔曲线,以法国工程师皮埃尔·贝塞尔的名字命名,由于它们的平滑和可控行为,在计算机图形学中得到了广泛应用。它们是 Blender 中的默认插值。关键帧与其邻居之间的当前 interpolation 存储为关键帧的 interpolation 属性:
>>> kf.interpolation
'BEZIER'
贝塞尔曲线的点有两个额外的坐标 – 一个左侧手柄和一个右侧手柄,它们都影响插值路径。为了支持曲线插值,Blender 关键帧包含两个额外的坐标,存储为 handle_left 和 handle_right 属性。与 co 属性一样,曲线手柄是二维点:
>>> kf.handle_left
Vector((16.0, 10.0))
>>> kf.handle_right
Vector((31.0, 10.0))
Blender 支持其他插值。它们覆盖非常特定的案例,并且在撰写本文时,它们在动画中并不常用。它们以用于其计算的数学函数命名,并在 API 文档中描述,在 docs.blender.org/api/3.2/bpy.types.Keyframe.xhtml 和 #bpy.types.Keyframe.interpolation:
-
QUAD: 二次缓动 -
CUBIC: 三次缓动 -
QUART: 四次缓动 -
QUINT: 五次缓动 -
SINE: 正弦缓动(最弱,几乎线性但略有曲率) -
EXPO: 指数缓动(戏剧性) -
CIRC: 圆形缓动(最强且最动态) -
BACK: 带有超调和稳定的立方缓动 -
BOUNCE: 指数衰减的抛物线反弹,类似于物体碰撞时的情况 -
ELASTIC: 指数衰减的正弦波,类似于弹性带
我们将在本章末尾回到关键帧;在此期间,我们将构建一个基于当前动画持续时间的场景播放设置工具。
在这些示例中,我们的脚本使用现有动画的属性。在下一节中,我们将使用 Python 创建动画。
编写 Vert Runner 附加组件
在本节中,我们将编写一个附加组件,该组件将选定的对象沿活动对象的几何形状动画化。动画将追踪连接网格顶点的路径,因此得名Vert Runner:

图 7.16:沿着路径顶点动画化玩具
这可以作为程序化行走或巡逻、运动效果或任何其他具有几何路径的情况的基础。
在此操作符中,选定的对象和活动对象被区别对待:活动对象是选定对象移动的参考几何形状。
设置环境
让我们从向我们的附加组件目录添加一个新的脚本开始:
-
在 VS Code 中选择
PythonScriptingBlender/ch7/addons。 -
通过点击新建 文件图标创建一个新文件。
-
将新文件命名为
vert_runner.py。 -
双击打开文件。
如同往常,我们将从附加信息开始。
编写 Vert Runner 信息
我们的新操作符可以通过选择位置信息来调用:
bl_info = {
"name": "Vert Runner",
"author": "John Packt",
"version": (1, 0),
"blender": (3, 00, 0),
"location": "Object > Animation > Vert Runner"
"description": "Run on vertices of the active object",
"category": "Learning",
}
我们将继续进行常规步骤:
-
编写操作符
-
编写菜单条目
-
注册类和接口
编写 Vert Runner 操作符
在import部分之后,我们必须创建VertRunner类及其bl_*标识符:
import bpy
class VertRunner(bpy.types.Operator):
"""Run over vertices of the active object"""
bl_idname = "object.vert_runner"
bl_label = "Vertex Runner"
bl_description = "Animate along verts of active object"
bl_options = {'REGISTER', 'UNDO'}
我们使用一个整数属性设置每个关键帧之间的距离:
step: bpy.props.IntProperty(default=12)
下一步是编写此操作符的poll和execute方法。
编写操作符方法
我们将根据运行所需操作的需要编写poll方法。
在 poll()方法中检查的要求
为了在活动对象的几何形状上动画化选定的对象,我们需要以下内容:
-
活动对象
-
网格数据
-
选定的对象
使用这些条件,要使poll()方法返回False,它们转换为以下内容:
@classmethod
def poll(cls, context):
obj = context.object
if not obj:
return False
if not obj.type == 'MESH':
return False
if not len(context.selected_objects) > 1:
return False
return True
如果没有满足任何return False条件,则 poll 成功。在这种情况下,操作符可以运行其 execute 方法。
编写 execute() 方法
将操作符的目标分解为步骤,我们应该做以下事情:
-
获取巡逻点的列表;在这种情况下,活动对象的顶点。
-
滚动通过选定的对象。
-
将它们通过巡逻点移动并设置关键帧。
我们首先将活动对象的顶点存储在一个列表中:
def execute(self, context):
verts = list(context.object.data.vertices)
当我们遍历选定的对象时,我们应该确保跳过活动对象,因为它可能被选中:
for ob in context.selected_objects:
if ob == context.active_object:
continue
然后,我们必须遍历顶点列表并为每个坐标设置关键帧,从当前帧开始。我们必须在每次迭代中递增 frame 数量:
frame = context.scene.frame_current
for vert in verts:
ob.location = vert.co
ob.keyframe_insert('location', frame=frame)
frame += self.step
return {'FINISHED'}
当 for 循环结束时,我们必须返回一个 'FINISHED' 状态并退出操作员。现在 VertRunner 类已经完成,我们可以开始处理其菜单条目。
编写菜单和注册函数
由于菜单元素以相反的顺序显示,我们必须首先添加一个 separator:
def anim_menu_func(self, context):
self.layout.separator()
self.layout.operator(VertRunner.bl_idname,
text=VertRunner.bl_label)
现在,是时候注册操作员和菜单,以便它可以从界面运行:
def register():
bpy.utils.register_class(VertRunner)
bpy.types.VIEW3D_MT_object_animation.append(
anim_menu_func)
def unregister():
bpy.types.VIEW3D_MT_object_animation.remove(
anim_menu_func)
bpy.utils.unregister_class(VertRunner)
如果我们刷新 插件 列表,我们将在 学习 类别中看到 Vert Runner。启用它将 Vert Runner 添加到 对象 | 动画 菜单:

图 7.17:对象 | 动画 | Vert Runner 对选定的对象进行动画处理
在选择至少两个对象后使用 Vert Runner 将会沿着活动对象顶点动画化选定的对象。我们可以添加一个选项来使动画循环,并动画化对象的旋转。
创建循环动画
有时,我们希望动画的第一帧和最后一帧匹配,这样我们就可以循环观看它——例如,一个角色在圆形中奔跑的无尽剪辑。
在我们的案例中,一个对象穿过网格的所有点,从第一个点开始,以最后一个顶点结束,因此动画的第一帧和最后一帧将不同。
为了创建动画循环,我们需要在通过最后一个顶点后添加一个额外的步骤,回到第一个坐标。
用户必须能够选择他们是否想要循环动画,因此我们将向我们的操作员添加一个选项。loop 属性是一个布尔属性——当操作员运行时可以启用和禁用:
class VertRunner(bpy.types.Operator):
"""Run over the vertices of the active object"""
bl_idname = "object.vert_runner"
bl_label = "Vert Runner"
bl_description = "Animate along verts of active object"
bl_options = {'REGISTER', 'UNDO'}
step: bpy.props.IntProperty(default=12)
loop: bpy.props.BoolProperty(default=True)
实现非常简单:在 verts 的末尾添加其第一个元素的副本,将对象在动画结束时带回到初始位置:
if self.loop:
verts.append(verts[0])
动画化旋转稍微复杂一些。借助一点数学知识,在每一帧,我们可以将对象朝向其下一个目的地定位。
添加旋转
旋转背后的数学知识一开始可能具有挑战性,但因为我们只想围绕对象的 Z 轴旋转对象,我们可以使用基本的 三角学。
在三角学中,角度可以表示为半径为 1 的圆的弧,因此最大长度等于两倍的 π。字母 π(发音为 pi)是圆与其直径的比例。其近似值是 3.14。
三角学是一个包含许多关于角度、线段和旋转之间关系的有用函数的框架。其中有一个函数回答了我们提出的问题——也就是说,我们如何旋转一个对象使其面向一个点?
表示旋转弧
想象将一个物体旋转到一个已知的X和Y坐标点。如果旋转在想象中的圆上画出一个弧线,我们可以将我们点的y坐标视为该弧线的高度。这个维度被称为该角度的正弦,当比较角度和长度时非常有用。
正弦的逆函数称为反正弦。它对我们来说很有趣,因为它与正弦相关的旋转。换句话说,如果我们想测量一个角度并且知道它的正弦值,我们可以使用以下表达式来找到旋转:
rotation = arcsin(sine)
我们知道正弦,即我们想要观察的点y坐标:

图 7.18:观察旋转的三角表示
因此,反正弦是我们正在寻找的三角函数。在 Python 中简写为asin,要使用它,我们必须从math模块中导入。
实现旋转
在三角学中,我们在代码中以3.14表示旋转,我们可以从math模块中导入pi常量。因此,除了asin之外,我们还需要pi,这样我们的导入部分看起来就像这样:
import bpy
from math import asin
from math import pi
我们将编写VertRunner.aim_to_point方法来单独处理物体旋转。为此,第一步是从目标坐标中减去当前位置,以便我们可以得到一个方向:
def aim_to_point(self, ob, point_co):
"""Orient object to look at coordinates"""
direction = point_co – ob.location
然后,我们必须规范化方向,以确保结果不受距离的影响:
direction.normalize()
观察旋转由asin(direction.y)返回,但有一个问题:反正弦始终假设它必须覆盖圆的右侧——即direction.x的正值。当我们的方向落在另一侧时会发生什么?

图 7.19:x 值为负时的观察旋转
在那种情况下,我们可以通过从asin结果中减去pi来到达圆的另一侧,因为pi表示单位圆周长的一半:
arc = asin(direction.y)
if direction.x < 0:
arc = pi – arc
我们还必须考虑到在 Blender 中,静止的物体看起来与Y轴的相反方向,因此我们必须在结果中添加一个顺时针旋转 90 度。
以弧度计,这是pi / 2:
arc += pi / 2
到目前为止,arc包含了我们正在寻找的旋转。我们可以立即使用它,但还有一个问题:有两种方式可以从一个旋转插值到另一个旋转。
寻找最短弧
想象将一个物体从 30°旋转到 330°。最快的方法是通过逆时针旋转,经过 0°并停止在-30°,这相当于 330°。最慢的方法是通过顺时针从 30°旋转到 180°,然后最终旋转到 330°:

图 7.20:从 30 度到 330 度的旋转的短弧和长弧
这两种都是从 30°到 330°的有效过渡,但我们可能希望选择最短的旋转:否则会导致对象自身旋转。
要找到远离当前旋转的最短弧线,我们必须在元组中存储三个可能性——目标方向、顺时针旋转一周后的相同值以及逆时针旋转后的相同值:
arcs = (arc, arc + 2*pi, arc - 2*pi)
然后,我们必须使用列表推导存储绝对旋转差。从那里,我们可以使用min获取最短弧线:
diffs = [abs(ob.rotation_euler.z - a) for a in arcs]
shortest = min(diffs)
我们必须使用与最小差值相关的弧。将此用作next语句的条件,我们可以找到它并将其分配给rotation_euler.z:
res = next(a for i, a in enumerate(arcs)
if diffs[i] == shortest)
ob.rotation_euler.z = res
现在,我们可以使用execute中的aim_to_point方法来动画旋转。
将所有这些放在一起
execute的最终版本只有一点不同。它以相同的方式开始:收集顶点列表,如果我们在动画循环,则再次添加第一个顶点,并跳过活动对象:
def execute(self, context):
verts = list(context.object.data.vertices)
if self.loop:
verts.append(verts[0])
for ob in context.selected_objects:
if ob == context.active_object:
continue
我们的方向方法基于当前对象位置,因此在开始动画之前,我们必须将对象移动到路径的末端:
ob.location = context.object.data.vertices[-1].co
这样,当动画开始时,aim_to_point会将对象朝向第一个顶点。现在,我们必须为rotation_euler.z插入关键帧,并重复此过程,直到所有点都被到达。之后,我们可以完成执行:
frame = context.scene.frame_current
for vert in verts:
self.aim_to_point(ob, vert.co)
ob.keyframe_insert('rotation_euler',
frame=frame, index=2)
ob.location = vert.co
ob.keyframe_insert('location', frame=frame)
frame += self.step
return {'FINISHED'}
通过在附加组件列表中启用Vert Runner或更新脚本(如果已经启用),我们可以在任何一对对象上测试我们的附加组件。
使用 Vert Runner
我们可以在每一对对象上使用这个操作符。在 Blender 中可用的实体中有一个独特的条目——一个风格化的猴子头,幽默地插入到更常见的形状如立方体、平面、球体等形状之间。由于猴子的头部有一个明显的正面,因此亲切地命名为苏珊,这使得旋转更容易可视化,因此用它来测试我们的附加组件是一个自然的选择:
-
通过从视图菜单栏选择添加 | 网格 | 猴子,将猴子头添加到场景中。
-
将任何其他网格添加到场景中,或者如果默认场景中存在,则使用立方体形状。
-
按住Shift(多选),选择猴子,然后选择用作动画向导的对象。
-
从视图菜单栏选择对象 | 动画 | Vert Runner。
-
使用Alt + A或点击媒体控制按钮播放动画。
立方体将穿过活动对象的每个顶点。速度和循环动画可以在选项中切换。
虽然相对简单,但这个工具可以扩展并生成车辆或甚至关节角色的动作。
动画编程通常是将直观的概念,例如朝向一个方向,转化为数学术语,就像我们在外推旋转时做的那样。此外,我们还研究了几何结构并获取了顶点坐标。
这使我们到达了本章的结尾,我们在这里学习了如何影响动画设置和对象的动画。
摘要
在本章中,我们熟悉了对象动画,学习了动画是如何创建和存储的,并查看哪些场景设置与动画系统直接相关。我们还学习了动画可以部分自动化,并从几何角度进行探讨,同时简要了解了旋转角度的三角函数表示。
能够自动化动画过程的一部分是一项宝贵的技能。有时,涉及的数学问题可能会出现并需要解决,但我们不应害怕,因为数学通常为大多数普通用例提供了一套现成的解决方案。
我们刚刚开始我们的生成动画之旅,这将在第八章中继续,我们将学习如何通过程序效果丰富动画曲线。
问题
-
动画值是如何存储的?
-
一个动画曲线能否包含整个矢量属性的关键帧?
-
动画曲线是如何分组的?
-
当前帧编号是 1。在不更改该设置的情况下,我们能否使用用户界面在帧 4 插入一个关键帧?
-
当前帧编号是 1。在不更改该设置的情况下,我们能否使用 Python API 在帧 4 插入一个关键帧?
-
平滑运动是否需要在每一帧都有关键帧?
-
关键帧是如何插值的?
-
有没有更多种方法来插值两个旋转?
第八章:动画修饰符
动画曲线,或 F 曲线,可以通过修饰符进行更改,而无需更改其关键帧。这样,电影或运动效果可以完全替换初始曲线,或添加到其原始值上。
修饰符的输出可以是另一个修饰符的输入,当结合使用时,允许我们在简单动画之上构建复杂的结果。
可以使用 Python 脚本来帮助自动化此过程并简化工作流程。
更改参数会影响修饰符的结果,而其整体影响可以通过修饰符界面中的滑块来减少。
在本章中,您将学习如何使用脚本添加修饰符到动画 F 曲线以及如何更改它们的参数。
本章将涵盖以下主题:
-
在 Blender UI 中理解 F 曲线修饰符
-
通过 Python 添加 F 曲线修饰符
-
在我们的插件中使用 F 曲线修饰符
技术要求
本章我们将使用 Blender 和 Visual Studio Code。本章创建的示例可以在github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch8找到。
使用 F 曲线修饰符
动画曲线的修饰符,称为F 曲线修饰符或F 修饰符,在保留原始数据的同时,为动画添加非破坏性更改。我们在第四章中考察了类似的功能,在对象约束中,我们学习了如何在不改变对象通道中存储的值的情况下影响对象的位置。
与对象约束类似,F 修饰符通过集合属性暴露给 Python 脚本。
在我们深入探讨如何编写 F 修饰符脚本之前,我们将看看如何在图形编辑器中创建它们。
在图形编辑器中添加 F 曲线修饰符
现在,我们将探讨如何使用 F 曲线修饰符为动画对象添加变化。
对于本例,我们将使用ani_loop.blend场景,来自配套的PythonScriptingBlender/ch8/_scenes_文件夹,但您可以使用任何场景。
在ani_loop.blend中的8字形路径上的动画并非手工创建:它是使用第七章中开发的Vert Runner插件生成的。
我们将通过在图形编辑器中创建 F 曲线修饰符来为动画对象的路径添加一些变化:
-
选择一个动画对象。
-
将 UI 区域之一更改为图形编辑器。一个好的位置是动画工作区中的左侧视图窗口。
-
在图形编辑器的左侧面板中,选择X****位置通道。
-
按N键显示属性选项卡。确保图形编辑器有焦点并且足够大,否则选项卡将不会显示。
-
在图形编辑器的右侧面板中,选择修饰符选项卡。
-
在修饰符选项卡中,从添加修饰符菜单中选择一个修饰符。在本例中,我们将使用步进****插值修饰符。

图 8.1:在图编辑器中添加曲线修饰符
Z 位置的动画曲线变为阶梯图。如果我们现在播放动画,我们会看到物体以小跳跃的方式前进,而不是像之前那样平滑。

图 8.2:在平滑曲线上应用了阶梯修饰符
Blender 手册在 F 曲线 修饰符 页面上详细描述了修饰符:
docs.blender.org/manual/en/3.2/editors/graph_editor/fcurves/modifiers.xhtml
有七种可用的类型。前两种基于数学公式生成曲线:
-
生成器:用于直线、抛物线和更高次曲线的表达式
-
内置:三角函数和对数公式
其他五个涵盖了基本动画任务:
-
信封:控制点用于编辑曲线的整体形状
-
循环:在动画的最后帧之后循环重复动画
-
噪声:向动画添加随机抖动
-
限制:将动画值限制在一个范围内
-
阶梯插值:将平滑动画转换为颠簸运动
与约束一样,F 曲线的修饰符作为集合暴露给 Python。我们可以使用 fcurve.modifiers.new 方法通过脚本添加新的修饰符。
在 Python 中添加 F 曲线修饰符
fcurve.modifiers.new(type) 方法根据提供的参数类型创建一个新的修饰符。它返回新的修饰符。
除了 FNGENERATOR 和 STEPPED 之外,给定类型的修饰符使用大写字母的类型名称创建:
type (enum in ['GENERATOR', 'FNGENERATOR', 'ENVELOPE', 'CYCLES', 'NOISE', 'LIMITS', 'STEPPED'])
因此,要向 2 添加一个 'STEPPED' 修饰符,我们使用以下方法:
>>> import bpy
>>> anim_data = bpy.context.object.animation_data
>>> m = anim_data.action.fcurves[2].modifiers.new('STEPPED')
同样,可以使用 fcurve.modifiers.remove 方法移除修饰符。这次,必须使用修饰符的 Python 实例作为参数:
>>> anim_data.action.fcurves[2].modifiers.remove(m)
现在我们已经了解了 F 修饰符的位置、工作原理以及如何在用户界面和 Python 控制台中添加更多修饰符,我们可以在脚本中使用这些知识。
我们将在下一节中编写的插件允许我们使用 F 修饰符创建摇晃动画。
编写 Shaker 插件
Shaker 插件通过向动画曲线添加噪声修饰符,在活动物体上创建摇晃效果。
有时候我们想在运动中添加一些摇晃。例如,导演经常使用 相机摇晃 来暗示物体被撞击或击中。另一个用例是车辆在崎岖路面上的运动,或者在风中飘动的毛发和羽毛。我们将编写的 Python 脚本将包含一个操作符和一个菜单函数,以便快速执行。
设置环境
我们首先为我们的插件创建一个 Python 脚本:
-
创建
PythonScriptingBlender/ch8/addons文件夹。我们可以使用文件管理器或代码编辑器的文件选项卡,例如VS Code。 -
在该文件夹中创建一个新文件,并将其命名为
object_shaker.py。我们可以使用文件管理器或代码编辑器的新建文件按钮。 -
使用您选择的编辑器打开文件。
-
在 Blender 的 文件 路径 首选项中设置
PythonScriptingBlender/ch8。
现在,我们将像往常一样开始编写插件代码。
编写 Shaker 插件信息
我们将在插件信息中添加我们的新操作符,location 属性:
bl_info = {
"name": "Object Shaker",
"author": "Packt Man",
"version": (1, 0),
"blender": (3, 00, 0),
"description": "Add Shaky motion to active object",
"location": "Object Right Click -> Add Object Shake",
"category": "Learning",
}
编写 Add Object Shake 操作符类
我们导入 bpy 模块,然后编写 Object Shaker 的 bl_* 标识符:
import bpy
class ObjectShaker(bpy.types.Operator):
"""Set Playback range to current action Start/End"""
bl_idname = "object.shaker_animation"
bl_label = "Add Object Shake"
bl_description = "Add Shake Motion to Active Object"
bl_options = {'REGISTER', 'UNDO'}
此操作符需要两个浮点参数:
-
噪声
duration以秒为单位 -
噪声
strength,即此修改器对动画的贡献程度
duration 应该是一个正数:不存在负时间量这样的东西。因此,我们将属性最小值设置为 0.0。另一方面,摇动的量可能从 0.0 或 1.0 以下的值中受益。这是一种更特殊的情况,我们仍然希望将值的范围从 0.0 到 1.0 设置为普通条件,但我们不希望阻止用户超出这些限制。我们可以设置适用于滑块的限制,但使用 soft_min 和 soft_max 参数接受超出范围的数值输入。
为属性添加限制和软限制
通常,修改器的强度或influence应该在 0.0 和 1.0 之间(分别表示没有影响和完全影响),但使用该范围之外的值会产生乘法效果。例如,2.0 的影响将使修改器的贡献加倍。
对于 Blender 属性的软限制在这种情况下很有用:min、max、soft_min 和 soft_max 限制界面中滑块的值域,但 min 和 max 从不接受超出其范围的任何数字,而 soft_min 和 soft_max 允许用户使用键盘点击滑块并输入他们想要的任何值。
超过初始 soft_min 和 soft_max 参数的值被视为有效输入,并成为滑块的新范围:
duration: bpy.props.FloatProperty(default=1.0, min=0)
strenght: bpy.props.FloatProperty(default=1.0,
soft_min=0,
soft_max=1.0)
现在,我们可以编写用于验证条件的 poll 方法以及用于执行添加噪声动作的 execute 方法。
编写操作符方法
除了常用的 poll 和 execute 方法外,我们还将编写一个用于查找给定属性 F-Curve 的实用函数。
编写 poll 方法
poll 方法的条件非常简单——如果存在活动对象,则可以调用操作符:
@classmethod
def poll(cls, context):
return bool(context.object)
我们需要一个动画属性来添加噪声修改器。如果它已经动画化,我们选择现有的动画曲线,否则,我们创建一个新的。这个操作可以作为一个单独的函数实现,命名为 get_fcurve,它接受 data_path 作为参数并返回其动画曲线。如果它还不存在,它将创建一个新的曲线。
编写 get_fcurve 方法
我们将查找或创建属性动画曲线的任务委托给get_fcurve函数。由于它将由ObjectShaker操作符单独使用,所以我们将其编写为一个类方法,self作为其第一个参数。我们可能希望对多个属性和对象使用它,所以我们还传递要检查的对象以及要动画化的属性的data_path。在矢量属性的情况下,我们还传递index组件。我们使用obj而不是object作为参数名称,因为后者代表 Python 的基本类,我们不想覆盖这个术语。
从第七章我们知道,F-Curves 属于动作,并且我们的操作符向当前动作添加噪声,因此这个函数将查找对象动画数据中的action属性。在我们运行get_fcurve之前,我们应该确保存在这样的动作,因此,按照在第六章学到的防御性编程实践,我们使用assert来停止脚本,如果由于不可预见的原因找不到当前动作:
def get_fcurve(self, obj, data_path, index):
"""Returns F-Curve of given data_path/index"""
action = obj.animation_data.action
assert action
现在我们需要返回由参数提供的data_path实例所动画化的 F-Curve,如果它不存在则创建它。我们可以尝试使用try来创建它,这是在第三章的改进我们的代码部分学到的语句。
尝试创建具有相同路径的两个 F-Curves 会导致RuntimeError错误,在try语句中,这会触发except子句。通过只在需要时查找现有曲线,我们的代码将更加精简,并且稍微快一点。
在except语句下,我们对一个条件迭代器使用next函数,即满足我们标准的一系列对象,在这种情况下,是一个匹配的data_path和index:
try:
crv = action.fcurves.new(data_path,index=index)
except RuntimeError:
crv = next(fc for fc in action.fcurves
if fc.data_path == data_path and
fc.array_index == index)
在任何情况下,我们都会得到包含我们正在寻找的 F-Curve 的crv变量。我们本可以使用for循环来迭代action.fcurves,但next函数提供了一个有效且紧凑的替代方案。
高效地滚动集合
next函数返回序列的第一个有效元素。例如,输入next(action.fcurves)简单地给出动作的第一个曲线。next的参数可以是任何迭代器,而不仅仅是列表或集合。由于迭代器可以包含条件语句,如if,因此next可以是一个简洁且高效的for循环的替代方案。
当fc for fc in action.fcurves滚动fcurves的所有元素时,对fc.data_path和fc.array_index的条件确保返回符合那些要求的第一条曲线。
如果找不到曲线,next将因StopIteration错误而失败,但我们知道这种情况不会发生:一个现有的曲线最初就带我们进入了这个try语句的except块。所以,无论是在try块下还是在except块下,crv变量现在包含我们正在寻找的 F-Curve。在我们向它添加修饰符之前,我们必须确保它至少包含一个关键帧。
确保存在关键帧
在这一点上,我们已经将动画曲线存储在crv变量中,但我们必须寻找其关键帧点,否则它将不会被评估。如果keyframe_points集合为空,我们将通过使用keyframe_points.insert向其中添加关键帧。我们将使用当前帧和值作为参数:
if not crv.keyframe_points:
crv.keyframe_points.insert(
frame=context.scene.frame_current,
value=getattr(obj,
data_path)[index])
现在我们已经有一个动画曲线,并且它保证支持修饰符,我们可以返回crv变量并退出get_fcurve函数:
return crv
这个函数将在execute方法中被调用,这是操作符的最后一块缺失的拼图。
编写execute方法
如果我们的对象尚未被动画化,我们创建新的animation_data,否则,我们将现有数据存储在anim变量中:
def execute(self, context):
if not context.object.animation_data:
anim = context.object.animation_data_create()
else:
anim = context.object.animation_data
同样,如果还没有创建新的动作,我们应该创建一个新的动作,或者获取当前的其中一个。在两种情况下,它都将被存储在action变量中:
if not anim.action:
action = bpy.data.actions.new('ShakeMotion')
anim.action = action
else:
action = anim_data.action
现在,是时候添加一些摇动动作了。首先,我们需要以帧为单位表达效果的持续时间,而不是以秒为单位。要做到这一点,我们需要将duration参数乘以场景的每秒帧数。一旦我们有了帧的持续时间,我们将它除以二,以便将对象摇动中心对准当前帧;一半的帧将在它之前播放,而另一半将在之后播放:
fps = context.scene.render.fps
duration_frames = self.duration * fps / 2
current = context.scene.frame_current
start = current – duration_frames
end = current + duration_frames
下一步是寻找我们想要修改的动画曲线:location Z,rotation_euler X,和rotation_euler Y。我们需要这些特定的曲线,因为它们分别代表了相机的上下摇动、偏航摇动和俯仰摇动。
如果它们不存在,我们的get_fcurve方法将创建并返回它们:
z_loc_crv = self.get_fcurve(context,
'location',
index=2)
x_rot_crv = self.get_fcurve(context,
'rotation_euler',
index=0)
y_rot_crv = self.get_fcurve(context,
'rotation_euler',
index=1)
由于 F-Modifiers 是针对每个曲线特定的,我们为每个曲线创建一个NOISE修饰符。我们使用一个for循环一次性创建所有三个。噪声strength值,一个浮点属性,可以直接从操作符的strength参数设置,而我们在之前已经计算了噪声的start和end值:
for crv in z_loc_crv, y_rot_crv, x_rot_crv:
noise = crv.modifiers.new('NOISE')
noise.strength = self.strenght
noise.use_restricted_range = True
noise.frame_start = start
noise.frame_end = end
return {'FINISHED'}
我们已经将use_restricted_range设置为开启,以限制start和end帧中的噪声:否则,frame_start和frame_end属性将没有效果。一旦我们为三个曲线设置了 F-Modifiers,我们就可以最终退出该方法。
现在我们已经完成了操作符,我们可以向界面添加一个菜单项,以及register/unregister函数。
添加菜单项
正如我们在编写接口时所学到的,一个菜单函数将self和context作为参数。
在菜单函数内部,我们向self.layout添加一个分隔符和ObjectShaker操作符:
def m_items(self, context):
self.layout.separator()
self.layout.operator(ObjectShaker.bl_idname)
这个功能可以添加到任何菜单中,但由于我们的操作员影响对象变换的动画,我们可以使用在对象模式中由视口显示的右键菜单。
查找上下文菜单的类名
API 文档不包含所有菜单的列表。我们可以在bpy.types中查找它们,它包含所有 Blender 类,并记住我们正在寻找的类名以VIEW3D_MT开头并以_context_menu结尾。
我们可以在列表推导中使用这些标准,即由方括号分隔的类似列表的对象,就像我们在本节前面遇到的next函数一样,它是由条件迭代器构建的。我们可以在 Blender 的Python 控制台中运行它:
>>> [c for c in dir(bpy.types) if
c.endswith('context_menu')]
在列出的上下文菜单中,我们找到了VIEW3D_MT_object_context_menu:
['ASSETBROWSER_MT_context_menu',
...
['VIEW3D_MT_edit_metaball_context_menu', 'VIEW3D_MT_gpencil_edit_context_menu', 'VIEW3D_MT_object_context_menu', 'VIEW3D_MT_particle_context_menu',
...
在返回匹配context_menu后缀的结果时,我们的列表推导几乎就像一个小型搜索引擎。为了进一步过滤结果,我们可以添加一个"object"字符串作为要求,以过滤输出到一个结果:
[c for c in dir(bpy.types)if c.endswith('context_menu')
and 'object' in c]
这个列表推导将结果缩小到仅对象上下文菜单:
>>> [c for c in dir(bpy.types)if c.endswith('context_menu')
... and 'object' in c]
['VIEW3D_MT_object_context_menu']
现在我们知道了要使用哪个菜单类,我们就可以继续注册这个插件了。
注册 Shaker 插件
启用插件会产生这两个结果:
-
将
ObjectShaker类添加到 Blender 中 -
将
m_items函数添加到对象的右键菜单中
这些任务都在register函数中发生:
def register():
bpy.utils.register_class(ObjectShaker)
bpy.types.VIEW3D_MT_object_context_menu.append(m_items)
按照相同的逻辑,当插件禁用时,其代码将从 Blender 中清除,此时应该从菜单中删除m_items,从已注册的类中删除ObjectShaker。未能这样做会在 Blender 中留下孤儿实体。unregister函数负责处理这一点:
def unregister():
bpy.types.VIEW3D_MT_object_context_menu.remove(m_items)
bpy.utils.unregister_class(ObjectShaker)
我们可以使用刷新按钮在插件首选项中刷新插件,并从学习类别中启用Object Shaker。当插件启用时,添加对象震动选项将出现在指定对象的右键菜单中。
使用 Shaker 插件
使用我们的插件,我们可以通过以下步骤为任何对象添加震动动作:
-
使对象激活。
-
右键点击(或者在首选项 | 快捷键中将使用鼠标按钮选择设置为右键时按
W)。 -
从菜单中选择添加对象震动。
-
在执行面板中调整持续时间和强度值。
就像在第七章中提到的Action Range 插件一样,所选的持续时间和强度可以在执行后使用编辑 | 调整最后操作从顶部栏进行更改。
我们创建了一个工具,它使用动画修改器为对象添加了程序化行为。这在用 Python 进行动画时是一个非常有价值的快捷方式。此外,它还向我们介绍了非破坏性修改器的概念,即添加可以随意删除或编辑的参数化更改。
概述
我们已经学会了如何为我们的场景创建动画效果,并看到了如何将一个想法转化为程序化工具。艺术家和技术动画师可以提出复杂的概念配置,我们可以根据本章概述的过程将其转化为快速设置的操作员。
使用动画系统是实现参数化行为的一种便捷方式,因为它依赖于应用程序的更新逻辑,并产生快速、可靠的输出。
我们将在第九章中探索一个类似但更强大的技术,从而完成我们对动画系统的概述。
问题
-
我们所说的非破坏性修改器是什么意思?
-
修改器会改变曲线的关键帧点吗?
-
我们能否将动画修改器添加到非动画对象中?
-
我们如何确保一个属性是动画化的?
-
参数软限制是什么?
-
在哪些情况下我们使用软限制而不是强限制?
-
我们如何在 Python 中查找类名?
第九章:动画驱动器
驱动器是一个控制属性值的函数。它可以以其他属性的值作为输入,从而在两个或多个属性之间建立连接。例如,一个驱动器可能会根据另一个物体的旋转设置物体的X位置。
驱动器与动画类似,它们共享更新系统和 f-curve 数据,但更加灵活,并且可以与 Python 结合创建自定义设置。
它们是技术动画的重要组成部分,用于创建简单的控制或复杂的机械。驱动器没有特定的用途:它们被设计来创建自定义行为。因此,它们在绑定中无处不在,有助于连接属性,甚至在不同类型的实体之间,如物体和着色器。
在本章中,你将学习如何轻松创建和测试你的 Python 驱动器,以及如何脚本它们的创建。除了帮助自动化绑定机制外,这些知识还将使你更容易理解公式并在 Blender 中实现它们。
本章将涵盖以下主题:
-
创建驱动器
-
在驱动器中使用 Python 表达式
-
脚本数学公式
-
自动化驱动器设置
技术要求
在本章中,我们将使用 Blender 和Visual Studio Code,但任何 IDE 都可以。本章创建的示例可以在github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch9找到。
创建驱动器
创建驱动器的步骤与创建动画的步骤非常相似。虽然动画时间是动画曲线的唯一输入,但驱动器可以依赖于以下一个或多个:
-
Python 表达式的结果
-
任何可以动画化的属性
-
物体的变换通道
-
物体之间旋转的差异
-
物体之间的距离
当我们创建一个驱动器时,我们必须指定至少一个输入。在本节中,我们将学习如何通过创建新的驱动器并使用用户界面来设置一个简单的轮子。
通过右键菜单创建快速驱动器
创建驱动器有一些快捷方式。
让我们通过一个例子来了解这些快捷方式。假设为了动画化一个轮子,我们想让一个物体的位置 Y驱动其旋转 X通道。我们可以为 Blender 的默认立方体设置这个:
-
打开 Blender 或通过文件 | 新建 | 通用返回默认场景。
-
选择默认的立方体使其变为活动状态。
-
按N显示变换属性。
位置 Y是我们的输入。我们不会寻找它的数据路径,而是将其复制到剪贴板:
-
右键点击位置 Y以显示Y: 菜单。
-
从菜单中选择复制为 新驱动器。
驱动器尚不存在,因此我们必须为我们要影响的属性创建它:
-
右键点击旋转 X以显示X: 菜单。
-
从菜单中选择 粘贴驱动器。
旋转 X 通道将呈现紫色,这是用于驱动属性的颜色。沿着立方体的 Y 轴移动 立方体 对象也会使其滚动:

图 9.1:Y 位置驱动 X 旋转
使用 5 弧度的驱动器相当于大约 286 度的旋转,正如我们在 图 9**.1 中可以看到的值所反映的那样。
将 旋转单位 切换到 弧度,如图 图 9**.2 所示,使 位置 Y 和 旋转 X 之间的一对一关系变得明显:

图 9.2:位置 X 和旋转 X 使用弧度显示相同的值
即使立方体在移动时滚动,它看起来也不像轮子:轮子是向相反方向旋转的。我们可以使用 驱动器 编辑器 区域来设置这一点。
使用驱动器编辑器设置轮子
在 驱动器编辑器 区域,我们可以显示和编辑场景中存在的对象的驱动器。可以通过以下步骤打开:
-
右键单击一个 驱动属性。
-
选择 打开 驱动器编辑器。
它与我们在 第七章 的第一 部分 中查看的 图形编辑器 区域非常相似,但它显示的是驱动器而不是动画曲线:

图 9.3:驱动器编辑器区域中的驱动器 f 曲线
我们可以通过从 驱动器编辑器 菜单栏选择 视图 | 全部帧 或按键盘上的 Home 键来选择更好地查看曲线。
默认的驱动器 f 曲线是正笛卡尔平面的对角线,控制点位于坐标 (0.0, 0.0) 和 (1.0, 1.0)。我们可以看到曲线的结果,显示在 5.0,与 位置 输入变量的相同值。
由于轮子的旋转方向与其运动方向相反,我们需要通过改变曲线来反转这个结果。为此,请按照以下步骤操作:
-
在 驱动器编辑器 属性中,选择 F 曲线 选项卡。
-
通过左键单击选择 f 曲线的右上角点。
-
在
1.0到-1.0:

图 9.4:驱动器的 f 曲线指向下方
现在,将立方体沿其 Y 轴移动,使其向正确的方向滚动。如果你仔细观察,会发现仍然有问题:这个驱动器滚动得稍微慢了一些。
大小为 1 的圆的周长与其在一圈内覆盖的长度之比是 π,即数学常数 π,大约为 3.14。
那个距离只需要半个圆周就能到达一个直径是轮子两倍的轮子,就像我们默认的 2 x 2 x 2 大小的立方体,所以在 1.571 中输入 pi/2,一个略快于 1 的乘数。现在,将立方体沿其 Y-轴移动,它就像轮子一样滚动,尽管是正方形的。
我们使用除法得到那个结果,但也可以在驱动器中使用 Python 公式。我们也可以仅通过输入一个公式来创建驱动器。
在属性中创建驱动表达式
可以通过在属性字段中输入一个哈希符号 (#) 创建一种不同类型的驱动器,它依赖于 Python 数学公式。以下是创建驱动表达式步骤:
-
打开 Blender 或通过 文件 | 新建 | 通用 返回默认场景。
-
选择默认的立方体以使其处于活动状态。
-
左键单击
rotation_euler.x属性以编辑其值。 -
输入
#sin(frame)并按 Enter 键:

图 9.5:在对象属性中输入 Python 表达式
我们刚刚编写的表达式已经生效。如果我们通过按 媒体控制 区域中的三角形 播放 按钮或使用 Alt + A 来开始播放,立方体将在其 X-轴上快速抖动。
sin(frame) 表达式依赖于时间,就像动画一样,但每帧的值是 Python 指令的输出,我们可以用它来获得更复杂的结果。
在下一节中,我们将结合 frame 变量、sin 函数和驱动输入来创建一个程序化、参数化的摆动动画。
我们不想要结果!
省略 # 符号将设置表达式的结果而不是创建一个驱动器。输入 frame 将属性设置为当前帧的数值,例如 1 或 24。如果我们输入 #frame,值将随着动画的播放而改变。
驾驶循环运动
摆锤是从固定点悬挂的重量,可以自由来回摆动。它在时间、重力和地理测量中有许多实际应用,而在 3D 中,振荡运动用于显示时钟机构、悬挂道具和其他循环运动。三角函数 正弦 通常用于模拟这种运动。
来自 math 模块的 sin 函数是 Python 语法中的 正弦。我们在 第七章 中遇到了 正弦,我们使用它的反函数,反正弦,通过 Python 定位对象。正弦是一个周期性波函数——它在固定间隔内重复:

图 9.6:正弦函数
播放动画会使立方体非常快速地抖动。为了减慢它,我们可以点击 驱动属性 并给公式一个更慢的速度。
例如,我们可以将其更改为 sin(frame/10),这将使速度减慢十倍。现在,立方体轻轻摇晃。我们可以做得更好,并为适当的摆动设置一个旋转支点。
通过约束更改旋转枢轴
影响对象枢轴的最简单方法是通过使用约束。我们在第四章中遇到了约束,并使用它们在不改变对象的变换属性的情况下改变对象的位置。这次,我们将使用枢轴约束来改变旋转中心。
在 Blender 中添加枢轴约束
枢轴约束将对象的旋转中心移动到另一个对象的位置或特定的坐标。我们将使用一个空对象,这是一个不包含任何几何形状的 Blender 对象:
-
通过从3D 视图区域选择添加 | 空对象 | 平面坐标轴将一个空对象添加到场景中。这个对象将成为新的旋转枢轴。
-
将这个空对象移动到立方体对象上方,使其可以作为悬挂点。
现在,我们可以创建约束:
-
选择立方体对象,并在属性中的约束选项卡中找到它。它有一个连接杆的图标。
-
从添加对象约束下拉菜单中选择枢轴。将创建一个新的约束:

图 9.7:创建枢轴约束
-
在枢轴约束面板中点击目标字段,并在对象列表中选择空对象。
-
现在,点击旋转范围属性并将其更改为始终,以便所有方向的旋转都会受到影响。
如果我们播放动画,立方体会左右摆动:

图 9.8:枢轴约束改变立方体对象的旋转中心
它开始看起来像是一个摆锤,但摆动的速度应该取决于绳子的长度,而这在我们的公式中并没有考虑。为了改进我们的驱动程序,我们必须了解正弦函数的工作原理以及如何控制其周期。然后,我们必须研究摆的物理,并编写一个考虑绳子长度的表达式。
控制正弦函数的周期
为了更好地控制正弦周期,我们需要观察其图形,如图图 9**.9所示。它在帧0时值为0,在帧1和2之间上升后,在帧3之后稍微回到零:

图 9.9:正弦函数
这是因为正弦,一个与角度相关的函数,依赖于数学常数π,其值在3.14、6.28等点为零。角度与圆之间的关系是由于角度如何描述圆弧。
如果正弦函数在每个完整圆周上重复,并且一个完整圆周为 2 * π弧度,那么我们可以说sin(frame)公式的周期是2 * pi帧。
通过将2 * pi作为sin函数的参数,我们得到一个周期正好为一帧的公式:
sin(frame * 2 * pi)
这个公式的结果始终是0,但这比看起来更有用:将frame * 2 * pi除以特定的帧数,我们可以设置公式重复所需的时间——也就是说,我们现在可以控制周期。
例如,以下公式的结果每 10 帧重复一次:
sin(frame * 2 * pi / 10)
现在,我们可以查找摆动公式并设置一个物理上正确的振荡。
实现摆动方程
根据维基百科(en.wikipedia.org/wiki/Pendulum),摆动的周期取决于其绳索的长度,并使用以下公式进行近似:
2π√(L/g)
它读取2乘以pi乘以length除以gravity。在 Python 中,它看起来如下:
2 * pi * sqrt(length / 9.8)
在这里,sqrt是平方根运算,9.8是按照国际单位制(SI)的地球重力。在这个系统中,时间单位是秒,因此我们需要在我们的驱动程序中用秒来表示公式。
在一帧中重复sin的表达式如下:
sin(frame * 2 * pi)
由于我们需要以秒为单位应用周期,我们将该表达式除以每秒帧数:
sin(frame/fps * 2 * pi)
这将我们的表达式减速到 1 秒的周期。
我们的目标是得到2 * pi * sqrt(length / 9.8)秒的周期,因此我们将sin的参数除以这个值。
除法后,我们得到一个关于两个变量fps和length的函数:
sin((frame / fps) * length/9.8)))
2 * pi / 2 *pi的值是1,可以从乘法中删除。现在,我们的公式看起来好多了:
sin(frame / fps / sqrt(length/9.8))
在旋转 X中输入它创建一个驱动程序:

图 9.10:摆动公式的实现
尽管如此,驱动程序还不能工作——frame变量已经被 Blender 定义,但length和fps不存在并导致错误。我们需要将这些两个变量添加到驱动程序属性中。
向驱动程序添加变量
我们可以在本章创建驱动程序部分的驱动程序编辑器区域执行此操作,但由于我们不需要编辑f-curve,我们可以使用一个更简单的界面。
显示驱动属性窗口
驱动属性窗口显示单个驱动程序的详细信息。它访问快捷,其内容与驱动程序区域中的驱动程序标签页相同。
显示和编辑驱动属性的步骤如下:
-
右键单击一个驱动属性。
-
从上下文菜单中选择编辑驱动程序。
驱动属性窗口总结了受影响属性的路径,它是哪种类型的驱动,如果驱动程序有错误,以及创建了哪些变量:

图 9.11:脚本表达式驱动程序的性质
在这个阶段,驱动类型将被设置为脚本表达式;表达式字段包含我们的摆动公式,而错误标签会通知我们 Python 表达式出了问题。
这个错误是由缺少fps和length变量引起的。添加它们将修复它。
稍等一下你的窗口!
驱动属性是一个弹出窗口,当鼠标指针移回其边界之外时消失。不过,不用担心;每次更改都会在您再次打开窗口时仍然存在。
获取每秒帧数属性
点击var,它是一个RNA 属性变量——也就是说,它从 Blender 中的另一个属性读取值。为了获取这个值,我们需要指定以下内容:
-
实体的类型(对象、场景、动作等)
-
实体的名称
-
属性的名称
这些属性可以在变量面板中设置:

图 9.12:一个新创建的变量
通过这个变量获取场景的每秒帧数渲染设置的步骤如下:
- 从属性类型列表中选择场景,该列表显示在变量名称下方的左侧按钮:

图 9.13:设置属性变量类型
-
一个 Blender 文件可以包含多个场景。我们可以通过右侧的列表按钮显示它们的列表并选择一个。默认场景的名称是场景。
-
现在场景已被选择,另一个字段
render.fps获取每秒帧数,如渲染设置中所设置。 -
我们必须通过点击位于遗传代码(RNA)图标右侧的当前名称将变量重命名为
fps:

图 9.14:渲染设置中的 fps 变量
现在,变量名称与驱动表达式中所使用的名称相匹配,因此frame/fps是当前帧的时间,以秒为单位。这允许下一个变量length通过基于秒的公式影响周期。
使用距离变量获取摆长
有四种类型的驱动变量:
-
单个属性
-
变换通道
-
旋转差异
-
距离
前两种,单个属性和变换通道依赖于属性值,而旋转差异和距离则是由两个对象的变换之间的差异产生的。
在这种情况下,绳子的长度是驱动对象与其支点之间的距离——即length:
-
通过点击+ 添加输入 变量按钮添加一个新变量。
-
点击 RNA 图标将类型更改为距离。面板将改变,允许您选择两个对象:

图 9.15:更改变量类型
- 选择
length:

图 9.16:摆动驱动器的变量设置
在动画播放时将 Empty 或 Cube 对象移近,会使摆动变快,而将它们分开则会减慢摆动。
正弦函数的最大值是 1,将其从 弧度 转换后,给出了此驱动器达到的最大角度:
>>> degrees(1)
57.29577951308232
这个值是振动的振幅。摆的振幅取决于其初始位置。在现实生活中,由于空气的摩擦,振幅会逐渐减小,直到摆达到其静止位置并停止。我们不在驱动器中实现空气阻力,但我们仍然可以添加一个控件来影响运动振幅。
控制振幅
当处理波形图时,振幅 这个术语有特定的含义,但对于我们的目标,我们可以将其视为运动的乘数。
由于我们正在为动画编程,我们的振幅控制在视觉上比在物理上更重要。
添加自定义属性
我们已经使用 empty 类型的对象控制了支点的位置。由于移动 Empty 已经改变了周期性运动,我们可以在它上面添加一个新的属性来控制振幅。这样,我们可以通过选择单个对象来影响摆的行为。
向对象添加属性的过程如下:
-
选择我们用作支点的 Empty 对象。
-
在 对象属性 面板中,找到 自定义 属性 部分。
-
点击 + 新建 按钮添加属性。它将默认命名为 prop:

图 9.17:向活动对象添加自定义属性
-
点击齿轮图标并将 属性名称 更改为 amplitude。
-
右键点击值(默认值
1.00是好的)并选择 复制 数据路径。
这将属性路径复制到剪贴板。这将在我们添加下一个驱动器变量时很有用。
在驱动器中使用自定义属性
将驱动器表达式乘以 amp 变量会影响其结果,并允许我们调节其振幅。要这样做,请按照以下步骤操作:
-
选择摆动的 Cube。
-
右键点击驱动旋转通道并选择 编辑驱动器 以打开 驱动 属性 编辑器。
-
点击
amp。 -
点击右侧的 Prop: 字段,并将属性对象选择为 Empty。
-
点击
["amplitude"]。方括号是 自定义属性 Python 路径的一部分:

图 9.18:自定义属性作为驱动变量
我们可以在驱动器表达式中直接添加 * amp,但我们可以做得更好:因为驱动器影响旋转,我们还可以在乘法中添加 pi:
sin(frame / fps / sqrt(length/9.8)) * amp * pi
sin函数在-1.0和1.0之间振荡,因此当1.0时,我们的驱动程序的结果在-pi到pi之间。
记住,完整圆弧的周长是2 * pi,因此期望一个pi值来描述半圆旋转的弧度是合理的。如果我们现在播放动画,我们会看到摆动振荡到其垂直方向——也就是说,向左旋转半圆,然后返回,再向右旋转半圆:1.0的振幅使摆动描述一个完整圆。
如果我们选择0.5,摆动将通过半圆弧。振幅等于0.25会得到更好的结果:每侧的最大旋转角度为 45 度;0.0的值将停止摆动。
从0.0(静止的摆)到1.0(整个旋转)的控制对动画师和 3D 用户来说有直接的意义,因为它允许他们通过改变振幅来设置他们想要的圆周分数。
我们在我们的驱动程序中使用了 Python 公式,但我们手动创建了整个设置。在下一节中,我们将编写一个 Python 附加组件来自动化此过程。
编写摆动附加组件
使用我们迄今为止所学到的知识,我们可以编写一个设置活动对象摆的附加组件。
我们将从第三章中的步骤开始,并为我们的附加组件创建一个.py文件。
设置环境
让我们在ch9文件夹中为第九章创建一个文件夹作为脚本文件夹属性,并重新启动应用程序。我们可以在我们的 IDE(本书中的 VS Code)中创建我们的新文件和文件夹,这样我们就可以开始编辑:
-
在VS Code中选择
PythonScriptingBlender/ch9/addons。 -
点击新建文件图标创建一个新文件。
-
将新文件命名为
pendulum.py。 -
双击文件以打开它。
我们现在可以添加大多数附加组件的标准元素:
-
附加组件信息
-
Operator类 -
菜单函数
-
注册函数
接下来,我们将学习如何编写这些信息。
编写信息
如同往常,关于我们的附加组件的信息将放入bl_info字典中:
bl_info = {
"name": "Object Pendulum",
"author": "John Packt",
"version": (1, 0),
"blender": (3, 00, 0),
"description": "Add swing motion to active object",
"category": "Learning",
}
这个字典只是为了 Blender 在列表中显示附加组件的名称和描述。下一步是编写Operator类。
编写 Operator 类
Operator类执行实际工作。我们派生bpy.types.Operator并在类的静态部分填写信息:
import bpy
class ObjectPendulum(bpy.types.Operator):
"""Set up swinging motion on active object"""
bl_idname = "object.shaker_animation"
bl_label = "Make Pendulum"
bl_description = "Add swinging motion to Active Object"
bl_options = {'REGISTER', 'UNDO'}
bl_idname以object.开头,因此它将被添加到bpy.ops.object操作符中。我们这样做是因为我们在操作符中做的所有事情都会影响对象级别的场景。
现在,我们必须将振荡参数添加到静态属性中:
amplitude: bpy.props.FloatProperty(default=0.25,
min=0.0)
length: bpy.props.FloatProperty(default=5.0, min=0.0)
它们将确定运动的振幅和长度变量。通过使用'REGISTER'和'UNDO'作为bl_options,操作符将允许实时更改:
bl_options = {'REGISTER', 'UNDO'}
现在,轮到poll方法了,在这里检查运行操作的条件。如果有活动对象,它必须返回True。我们可以使用bool函数即时转换context.object:
@classmethod
def poll(cls, context):
return bool(context.object)
最后,我们有execute方法。它执行本章前一部分的所有操作:
-
创建枢轴对象
-
添加一个用于振幅的自定义属性
-
使用单摆公式和变量创建驱动器
在函数开始时,我们将活动对象存储在ob变量中,然后创建一个新对象,该对象将是枢轴。在第二章中,我们了解到新对象可以分两步创建:
-
通过
bpy.data.objects.new获取新对象。 -
将对象链接到场景中存在的
Collection。
我们将使用context.collection并将枢轴链接到活动集合:
def execute(self, context):
ob = context.object
pivot_name = f"EMP-{ob.name}_pivot"
pivot = bpy.data.objects.new(pivot_name, None)
context.collection.objects.link(pivot)
使用None作为new的第二个参数创建一个没有几何数据变换——即matrix_world。枢轴应该放置在活动对象上方,因为location存储在变换矩阵的第四列,我们可以提高[2][3]的值):
pivot.matrix_world = ob.matrix_world
pivot.matrix_world[2][3] += self.length
现在,是时候添加自定义属性了。我们已经看到,我们的*振幅*属性的数据路径是["振幅"]。这是因为 Python 对自定义属性的访问遵循与 Python 字典相同的语法。
在 Python 字典中,dictionary["new_key"] = new_value语法添加了一个新项。同样,创建振幅浮点属性并将其分配给操作员同名参数的 Python 代码如下:
pivot["amplitude"] = self.amplitude
振幅现在将出现在我们的枢轴对象下。我们将在驱动器中使用它。现在,我们将向活动对象添加一个枢轴约束:
constr = ob.constraints.new('PIVOT')
constr.target = pivot
constr.rotation_range = 'ALWAYS_ACTIVE'
现在,是时候创建我们的驱动器了。驱动器作为对象,比约束稍微复杂一些,因为它们包含其他实体,例如 f 曲线,并且是动画数据的一部分。因此,我们不会使用来自animation_data的drivers.new方法,而是求助于对象的driver_add方法,该方法设置所有要求。它返回驱动曲线:
driver_crv = ob.driver_add('rotation_euler', 0)
driver = driver_crv.driver
谁是驱动器的驱动者?
driver_add方法返回 f 曲线而不是驱动器本身。实际的驱动器可以通过curve.driver属性访问。这使得访问新曲线变得更容易,但可以合理地期望driver_add返回驱动器。
我们的驱动器使用 Python 表达式,因此我们必须设置type和expression属性:
driver.type = "SCRIPTED"
xpr = "sin(frame/fps/sqrt(length/9.8)) * amp * pi"
driver.expression = xpr
可以使用variables.new添加fps、length和amp变量。
一旦我们创建了一个变量,我们就可以设置其目标。当前context.scene.render.fps属性是render.fps,所以只有一个目标。我们将变量类型设置为单个属性,并填写targets[0]的id_type、id和data_path:
fps = driver.variables.new()
fps.name = "fps"
fps.type = "SINGLE_PROP"
fps.targets[0].id_type = 'SCENE'
fps.targets[0].id = context.scene
fps.targets[0].data_path = "render.fps"
我们的单摆长度是pivot和ob之间的距离,因此它有两个目标:
len = driver.variables.new()
len.name = "length"
len.type = "LOC_DIFF"
len.targets[0].id = pivot
len.targets[1].id = ob
最后,我们可以看看振幅。它是枢轴的一个自定义属性,变量类型为'SINGLE_PROP',但这次,id_type是 Blender 对象。一旦驱动器设置完成,我们可以通过返回'``FINISHED'状态来退出函数:
amp = driver.variables.new()
amp.name = "amp"
amp.type = "SINGLE_PROP"
amp.targets[0].id_type = "OBJECT"
amp.targets[0].id = pivot
amp.targets[0].data_path = "["amplitude"]"
return {'FINISHED'}
ObjectPendulum类现在已经完整,因为它现在涵盖了整个设置过程。像往常一样,我们还必须将其添加到 Blender 的一个菜单中,以便更容易启动。
编写菜单和注册类
在第三章,我们学习了我们可以通过编写菜单函数将我们的条目添加到菜单中。self 和 context 参数分别是菜单实例和应用上下文。我们必须将操作员的bl_idname添加到菜单的layout中:
def menu_func(self, context):
self.layout.separator()
self.layout.operator(ObjectPendulum.bl_idname)
然后,在注册函数中,我们必须将menu_func添加到 Blender 的一个菜单中。在这个例子中,我们将使用对象模式中可用的右键单击菜单。我们在第八章中学习了如何查找菜单类名,对象上下文菜单类是VIEW3D_MT_object_context_menu。我们还必须注册操作员类ObjectPendulum:
def register():
bpy.utils.register_class(ObjectPendulum)
ob_menu = bpy.types.VIEW3D_MT_object_context_menu
ob_menu.append(menu_func)
当对象摆锤附加组件启用时,这为我们添加了新的功能。当然,当它被禁用时,我们必须撤销这些操作来清理我们的附加组件元素:
def unregister():
ob_menu = bpy.types.VIEW3D_MT_object_context_menu
ob_menu.remove(menu_func)
bpy.utils.unregister_class(ObjectPendulum)
现在我们这个附加组件已经准备好了,设置摆锤的步骤如下:
-
在对象模式下,选择一个对象使其变为活动状态。
-
右键单击并选择创建摆锤来调用附加组件。
-
在操作员属性中设置
length和amplitude的值。
编写这个附加组件将使你在前几章中学到的许多技术得以应用。驱动器是脚本编写的一个非常富有创造性的领域,而这只是我们能够用它们做到的一小部分。
摘要
驱动器是位于动画、绑定和编程交叉路口的强大工具。一方面,它们可以包含 Python 表达式并自行实现自定义机制,另一方面,整个驱动器设置过程可以通过脚本自动化。
本章中我们编写的工具是一个小的自动绑定工具,它可以在任何 Blender 对象上复制相同的机制,并具有可编辑的参数。
能够结合驱动器、约束和自定义属性,以及自动化整个流程,是 3D 制作的一个基本部分,因为它允许非技术用户继续进行技术任务。
作为额外的好处,通过使用 Python,我们将一个物理公式转换成了一个可工作的驱动器表达式,这项任务有时可能会让人感到害怕,但可以通过观察和一点独创性来完成。
这个主题结束了我们对动画系统的探索。在下一章第十章,我们将学习我们的操作员如何与用户交互并监听事件。
问题
-
接口上用于驱动属性的颜色是什么?
-
我们能否为紫色属性设置关键帧?
-
一个度量属性,例如位置,能否驱动一个角属性,例如旋转?
-
我们能否改变驱动属性和被驱动属性之间的比率?
-
当我们在界面中设置值时,能否输入 Python 表达式?
-
我们如何告诉 Blender 我们输入的表达式应该是一个驱动器?
-
我们如何在用户界面中编辑驱动器属性?是否只有一种方法?
-
我们能否向对象添加自定义属性并使用它们来控制其他对象?
-
在 Python 中,我们能否使用
collection.new方法创建新的驱动器,就像我们使用约束一样?如果可以,为什么我们使用object.driver_add而不是其他方法? -
为什么驱动器变量的
targets属性是一个列表?哪种类型的变量有多个目标?
第十章:高级和模式操作符
自从第三章以来,我们就求助于操作符在 Blender 中实现我们的功能。
操作符已经证明具有极大的灵活性,具有自定义属性和外观。一旦我们学会了如何覆盖它们的所有方法,它们将变得更加强大。
在前几章中遇到的操作符在启动后立即运行并立即完成。如果我们需要,我们可以使执行模式化,并让操作符监听输入事件。
在本章中,你将学习如何控制操作符的执行,以及如何编写完全交互式的操作符。
本章将涵盖以下主题:
-
理解操作符流程
-
以编程方式设置属性
-
编写模式操作符
技术要求
在本章中,我们将使用 Blender 和 Visual Studio Code,但任何其他程序员文本编辑器都可以使用。本章创建的示例可以在github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch10找到。
理解操作符流程
我们从第三章开始处理操作符,并学习了它们的poll方法如何检查操作符是否可以执行,而execute则执行操作并退出。
然后在第四章中,我们通过'REGISTER'和'UNDO'选项将可编辑参数添加到了电梯操作符中。
我们还了解了一个聪明的技巧,当用户更改参数时可以实时更改结果——Blender 秘密地撤销最后一个操作并再次执行它,因此需要'UNDO'。
这在前面的第七章中变得更加明显,当时我们学习了如何从菜单栏中使用编辑 | 调整最后一个操作来更改最后一个操作的结果。
虽然这些解决方案使我们能够轻松获取输入参数,但它们并不提供访问实际输入事件的能力,例如按键的压力或鼠标的移动。
这将需要立即的execute方法,因此它不可能等待输入;因此,事件必须由另一个方法处理。
在execute中我们还不能做的事情是设置操作符的可编辑参数。由于 Blender 在参数更改时再次运行execute,用户会发现他们无法设置属性,因为它会被立即覆盖。
捕获事件和初始化操作符参数是execute无法执行的两个任务。幸运的是,当操作符启动时,涉及的不仅仅是execute方法;我们将看到操作符的生命周期包括一系列方法,每个方法都有其特定的目的。
执行步骤
从第三章中我们知道,操作符的poll和execute方法分别用于验证和执行。在第五章中,我们使用了invoke来确保操作符属性在运行之前显示。
现在,我们将更详细地看看操作符是如何显示和运行的:
-
Blender 检查
poll的返回值。如果结果是False,则操作符会被灰色显示;否则,操作符可以被启动。 -
操作符被启动,并运行
invoke方法。此方法为可选;如果我们不编写它,其步骤将被跳过,Blender 将直接运行execute。
invoke的一个常见用途是初始化操作符的变量或内部值;与传统的 Python 类不同,操作符不实现传统的__init__方法。
与execute一样,invoke必须返回一个退出状态,可以是'FINISHED'、'CANCELLED'或'RUNNING_MODAL'。
-
如果我们的操作符旨在监听鼠标和键盘事件,在
invoke中,我们将它添加到应用程序'RUNNING_MODAL'。 -
如果操作符是处理程序的一部分,它的
modal方法会在每次触发事件(当鼠标光标移动、按键按下等)时执行,直到 modal 返回'CANCELLED'或'FINISHED'。否则,为了继续监听,它应该返回'RUNNING_MODAL'。 -
如果
bl_options是{'REGISTER','UNDO'},则操作符属性将在屏幕左下角的面板中显示。该面板依赖于操作符的draw方法。 -
默认情况下,所有在声明时未标记为
hidden的操作符属性都会在面板中显示。重新实现此方法允许我们使用在第五章中学到的技术来实现自定义设计。 -
在操作符面板中更改值会再次运行
execute,并带有更新后的属性。
执行流程总结在图 10**.1中,这有助于我们理解当操作符启动时,执行流程中的方法是如何累加的。

图 10.1:从评估开始到结束的操作符方法
虽然poll在每次操作符显示时都会由界面运行,但invoke是操作符流程的第一步,因此我们可以用它来以编程方式设置操作符的参数,并从那里移动到execute或modal。在下一节中,我们将使用invoke根据一天中的时间初始化操作符参数。
编写“PunchClock”插件
一些工具可能需要从操作系统的时钟中获取当前日期和时间。在 Python 中,我们可以使用datetime模块在我们的脚本中获取它们,通常用于版本控制或日志记录目的。Blender 没有专门为时间单位设计的属性,但小时和分钟可以存储为操作符的两个单独的整型属性。
我们知道如何使用default参数来声明属性的初始值,但如果这个值并不总是相同怎么办?例如,一天中当前的小时和分钟会变化,但default只设置静态值。
但由于invoke方法在所有其他方法之前执行,我们可以在其中以编程方式设置默认值。
为了演示这一点,我们将创建一个附加组件来在当前场景中创建时间格式文本。默认情况下,文本显示一天中的当前时间,但用户可以更改它。
创建附加脚本
让我们在 Python 项目中创建ch10文件夹,然后在Blender 首选项中将其设置为脚本文件夹,并重新启动 Blender:
-
在你的文件浏览器或程序员编辑器中选择
PythonScriptingBlender/ch10/addons– 例如,VS Code。 -
点击新建图标创建一个新文件。
-
将新文件命名为
punch_clock.py。 -
打开文件进行编辑。
-
在 Blender 的文件路径首选项中设置
PythonScriptingBlender/ch10,并重新启动 Blender。
我们像往常一样在bl_info字典中存储附加信息:
bl_info = {
"name": "Text PunchClock",
"author": "Packt Man",
"version": (1, 0),
"blender": (3, 00, 0),
"description": "Create an Hour/Minutes text object",
"category": "Learning",
}
此附加组件包含一个操作符,它将在 3D 视图的添加菜单中可用。
我们从一个创建HH:MM格式文本的操作符开始,就像数字时钟一样,其中HH代表两位数的小时数,MM代表分钟。
小时和分钟存储为IntProperty,小时的范围是0到23,分钟的范围是0到59。操作符的代码开始如下:
import bpy
class PunchClock(bpy.types.Operator):
"""Create Hour/Minutes text"""
bl_idname = "text.punch_clock"
bl_label = "Create Hour/Minutes Text"
bl_description = "Create Hour Minutes Text"
bl_options = {'REGISTER', 'UNDO'}
hour: bpy.props.IntProperty(default=0, min=0, max=23)
mins: bpy.props.IntProperty(default=0, min=0, max=59)
如果 Blender 处于poll状态,我们可以添加一个新的对象,如下所示:
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'
在execute中,我们创建新的文本数据并将其设置为{hour}:{min}。Blender 中文本的类型名为FONT,其显示的文本存储在body属性中。
在变量后面使用:02表示我们想要显示一个两位数 – 例如,f"{3:02}"变为"03":
def execute(self, context):
txt_crv = bpy.data.curves.new(type="FONT",
name="TXT-clock")
txt_crv.body = f"{self.hour:02}:{self.mins:02}"
我们创建一个对象将其链接到当前集合,并查看场景中的文本:
txt_obj = bpy.data.objects.new(name="Font Object",
object_data=txt_crv)
之后,我们将执行状态返回为FINISHED:
context.collection.objects.link(txt_obj)
return {'FINISHED'}
我们的第一个操作符草稿已经准备好了,现在我们创建一个menu函数来添加到界面中。我们可以在搜索字段中使用time,最终得到三个相关的图标 – TIME、MOD_TIME和SORTTIME。任何这些都可以;在这个例子中我们将选择TIME。

图 10.2:图标查看器中的时间相关默认图标
我们在menu_func中使用separator开始,以将我们的操作符与其他内容区分开来,然后通过Layout.operator添加我们的PunchClock条目:
def menu_func(self, context):
self.layout.separator()
self.layout.operator(PunchClock.bl_idname, icon='TIME')
最后,我们在register和unregister函数中添加和移除我们的操作符和菜单项:
def register():
bpy.utils.register_class(PunchClock)
bpy.types.VIEW3D_MT_add.append(menu_func)
def unregister():
bpy.types.VIEW3D_MT_add.remove(menu_func)
bpy.utils.unregister_class(PunchClock)
如果我们重新启动 Blender 或刷新插件列表,我们应该能够看到PunchClock插件。

图 10.3:在插件列表中显示的 PunchClock
在这个阶段,在 3D 视图顶部的菜单中选择添加 | 创建小时/分钟文本会添加一个显示时间00:00的文本对象。
我们可以从datetime获取当前时间并将其转换为文本,但我们可以做得更好——通过在invoke中设置self.hour和self.mins,我们将达到相同的结果,同时允许用户更改显示的时间。
使用invoke初始化属性
要获取当前时间,我们在脚本开头导入datetime。导入部分变为以下内容:
import bpy
import datetime
然后,在操作符类内部,我们实现invoke方法。它可以直接跟在poll之后,但只要在PunchClock类下面都可以:
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'
def invoke(self, context, event):
now = datetime.datetime.now()
self.hour = now.hour
self.mins = now.minute
return self.execute(context)
现在,操作符的小时和分钟在invoke中设置,然后调用execute以继续操作。
使用execute完成操作很重要,因为 Blender 在更新操作时间顺序时期望这样做。
现在启动创建小时/分钟文本会显示当前时间在一个新的文本对象中,并允许我们使用操作符面板更改小时和分钟。

图 10.4:添加可编辑的小时和分钟字段,设置为当前时间
通过使用invoke,我们以编程方式设置了默认值。这在生产中是一个常见请求,因为所需的默认值可能会随着项目、任务和部门的不同而变化。
我们直接将我们的操作符添加到execute方法中。
为了防止菜单跳过invoke,我们需要在我们的菜单函数中覆盖布局的上下文。
在弹出菜单中确保默认的invoke
布局元素可以传递自定义上下文并强制对操作符进行设计选择。例如,显示在视口外的按钮会避免显示操作符属性,弹出菜单会绕过invoke方法。
我们在第五章的显示按钮部分和第七章的编写 Action To Range 插件部分遇到了这种行为。
我们通过在invoke中调用属性对话框或使用invoke来运行操作符,即使操作符是从invoke启动的,分别解决了这些问题。
因此,我们将布局的operator_context更改为"INVOKE_DEFAULT"。我们只需要为PunchClock这样做,因此,为了最小化对其他菜单项的潜在影响,我们添加了一个新行并仅更改其operator_context。
我们的下拉菜单函数变为以下内容:
def menu_func(self, context):
self.layout.separator()
row = self.layout.row()
row.operator_context = "INVOKE_DEFAULT"
row.operator(PunchClock.bl_idname, icon='TIME')
通过使用默认上下文执行PunchClock,我们确保invoke永远不会被跳过。
现在,操作员将始终显示其属性并允许用户更改它们,但我们也可以实现一种通过移动鼠标来更改显示时间的方法。
在下一节中,我们将添加操作员对鼠标和键盘输入的响应,使我们的操作员成为 modal 应用程序处理程序。
添加 modal 行为
在用户界面中,术语modal指代一个子窗口或小部件,它为自己获取所有用户交互,直到操作被明确结束。
通常,操作员被设计为立即返回到主应用程序。如果我们不希望这样,它们应该被添加到窗口管理器的 modal 处理程序中。
然后,操作员被视为 modal,并将监听用户输入,直到手动关闭。
我们可以使PunchClock成为 modal,并使用鼠标移动来设置我们的时钟。modal 操作员有两个要求:
-
invoke将操作员添加到处理程序并返回'RUNNING_MODAL'。 -
modal已实现并返回'RUNNING_MODAL'。当用户结束操作时,它返回'FINISHED',或者返回'CANCELLED'以无更改退出。
我们将通过更改invoke及其返回值来开始实现 modal 执行。
将操作员添加到 modal 处理程序
现在,invoke不再将参数传递给execute,而是调用当前window_manager的modal_handler_add方法,然后返回{'RUNNING_MODAL'}。返回状态通知操作员正在 Blender 中运行并监听事件。
由于modal在每次窗口更新时都会运行,我们应该保持它轻量级和小型。向场景添加对象是昂贵的,因此我们在invoke中创建和链接文本,并在modal中仅编辑其主体。invoke方法将txt_crv和txt_obj作为操作员成员属性存储:
def invoke(self, context, event):
now = datetime.datetime.now()
self.hour = now.hour
self.mins = now.minute
self.txt_crv = bpy.data.curves.new(type="FONT",
name="TXT-hhmm")
self.txt_obj = bpy.data.objects.new(name="OB-Txt",
object_data=self.txt_crv)
context.collection.objects.link(self.txt_obj)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
可以作为状态返回的关键字列在 API 文档中(docs.blender.org/api/3.3/bpy_types_enum_items/operator_return_items.xhtml),如下所示:
-
RUNNING_MODAL: 保持操作员在 Blender 中运行 -
CANCELLED: 操作员在没有做任何事情的情况下退出,因此不应推送任何撤销条目 -
FINISHED: 操作员在完成其动作后退出 -
PASS_THROUGH: 不做任何事情并将事件传递下去 -
INTERFACE: 已处理但未执行(弹出菜单)
我们已经处理了'RUNNING_MODAL'、'CANCELLED'和'FINISHED',而'PASS_THROUGH'对于将事件传递到应用程序的其余部分非常有用,即使我们的脚本正在监听它。'INTERFACE'用于弹出菜单,但通常我们不需要在我们的脚本中使用它。
状态并非一切!
重要的是要知道,返回状态确认了方法中完成的工作,但它本身并不执行任何操作。
例如,仅返回'CANCELLED'并不能撤销方法中执行的操作;我们应该通过编程方式撤销所有更改——例如,删除方法可能创建的对象,然后返回'CANCELLED'。
现在应用程序处理程序将查找modal方法并运行它,我们可以继续为我们的操作员编写一个。
编写modal方法
一旦将操作员添加到处理程序中,窗口管理器将在用户界面的每个事件上运行其modal方法。与invoke一样,除了self和context之外,此方法还接受第三个参数——event。
event参数包含有关触发任何modal执行的触发器的信息。它可以是鼠标移动或按键。
最相关的信息是type,这是一个字符串,其关键字在docs.blender.org/api/3.3/bpy_types_enum_items/event_type_items.xhtml上有文档说明。
查看event.type,我们可以找出触发更新的原因,如下所示:
event.type == "MOUSEMOVE"
这意味着用户刚刚移动了鼠标。
如果事件是由键盘引起的,event.type将是一个字母,例如"A",或者是对键的描述,例如"LEFT_CTRL"。与数字键相关的事件类型是该数字的大写字母——例如,"THREE"。
在这个例子中,将鼠标向右移动会增加当前时间,而向左移动则会减少它。
就像真实的时钟一样,我们可以设置小时或分钟——我们添加一个布尔属性来区分两者。属性部分变为以下内容:
hour: bpy.props.IntProperty(default=0, min=0, max=23)
mins: bpy.props.IntProperty(default=0, min=0, max=59)
set_hours: bpy.props.BoolProperty(default=True)
现在,我们终于可以编写PunchClock.modal了。
移动鼠标更新与光标相关的属性。例如,水平轴上的光标位置存储为mouse_x,而之前的位置仍然可用,作为mouse_prev_x。两者的差值给出了移动方向。
我们将那个数字存储为delta,然后除以它来减缓转换。10 倍因子足以达到我们的目的:
def modal(self, context, event):
if event.type == 'MOUSEMOVE':
delta = event.mouse_x - event.mouse_prev_x
delta /= 10
delta是一个浮点数,因此它不能与小时和分钟(整数)相加。因此,我们将它四舍五入到整数值:
delta = round(delta)
我们使用round而不是int进行此转换。因为int近似于最接近或等于的整数值,这会使从一个值到下一个值的进度不够平滑。
set_hours的值决定了delta是添加到小时还是分钟:
if self.set_hours:
self.hour += delta
else:
self.mins += delta
txt = f"{self.hour:02}:{self.mins:02}"
self.txt_crv.body = txt
要更改set_hours,我们求助于按键。我们通过按Tab键让用户在小时和分钟之间切换。
要获取那个按键,我们确保event.type是'TAB'且event.value是'PRESS':
if event.type == 'TAB' and event.value == 'PRESS':
self.set_hours = not self.set_hours
布尔变量只能是True或False,它们是对方的否定。因此,我们通过仅使用not将set_hours转换为它的对立面。
类型不足以!
按下的键也会释放,这个动作将生成另一个事件,其值为'RELEASE'。仅检查event.type而不检查event.value会使我们的代码面临响应按键两次的风险。
最后,当用户对显示的时间满意时,他们可以按下Return键并退出。按下Return键会触发类型为'RET'的事件。我们不需要担心退出事件中的event.value。一旦我们返回{'FINISHED'},操作员就会停止,因此没有重复执行的风险:
elif event.type == 'RET':
return {'FINISHED'}
然而,如果用户犹豫不决并想在不做任何事情的情况下退出工具?我们可以允许在按下'ESC'时取消操作。
要做到这一点,操作员必须在invoke中删除创建的文本后自行清理,然后返回{'CANCELLED'}以避免被添加到撤销队列:
elif event.type == 'ESC':
bpy.data.objects.remove(self.txt_obj)
return {'CANCELLED'}
那是我们操作员覆盖的最后一个事件。我们忽略任何其他事件,并默认返回{'RUNNING_MODAL'}作为状态以继续监听。
因此,modal的最后一条通常是以下内容:
return {'RUNNING_MODAL'}
调用重新加载脚本,然后添加 | 创建小时/分钟文本,将创建当前小时的文本并开始监听鼠标/键盘事件。左右移动鼠标会增加/减少当前值,按下Tab键在小时和分钟之间切换,按下Return或Esc键结束操作员。
由于所有动作现在都在invoke和modal之间进行,我们可以删除execute,但由于bl_options设置为{'REGISTER', 'UNDO'},Blender 会显示操作员属性。当属性更改时,会运行execute方法。
我们可以看到,按下返回键后,小时、分钟和设置小时可以在操作面板中更改。

图 10.5:模态退出后的操作员属性
那个面板可以自定义 - 操作员有一个draw方法,它的工作方式与我们在第五章中了解到的Panel.draw相同。
因此,我们可以在操作面板中以时间格式显示小时和分钟,而不是按列显示。在下一节中,我们将实现draw方法并更改创建小时/分钟 文本面板。
操作面板的样式
我们知道在这些情况下操作面板会显示:
-
当显式调用
context.window_manager.invoke_props_dialog时。 -
当
bl_options设置为{'REGISTER', 'UNDO'}并且操作员已完成时。 -
当
bl_options = {'``REGISTER', 'UNDO'}。
默认情况下,所有属性都按列布局显示。大多数属性类型可以用hidden=True标志声明,但BoolProperty不是这种情况,所以我们不能在set_hours中这样做。
作为一种解决方案,我们可以将set_hours改为IntProperty,范围是0到1,并将hidden设置为True,但通过为我们的操作符实现draw方法,我们可以简单地省略我们不想显示的属性。
编写绘制方法
我们想在创建小时/分钟****文本面板中更改两件事:
-
小时和分钟应在同一行
-
set_hours内部变量不应显示
向操作符添加draw方法会改变其布局。方法参数是self和context,但我们只会使用第一个。为了在同一行显示我们的属性,我们将执行以下操作:
-
创建一行来显示小时和分钟。
-
为新的
row启用align。 -
将对齐设置为
'CENTER':
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.alignment = 'CENTER'
- 使用
row.prop显示self.hour,使用row.label显示分号,然后再次使用row.prop显示self.mins:
row.prop(self, 'hour', text="")
row.label(text=' :',)
row.prop(self, 'mins', text="")
我们将hour和mins的文本设置为"",因为没有必要进行解释。正如预期的那样,没有显示set_hours的复选框,因为它在draw中没有提到。

图 10.6:自定义小时/分钟操作面板
我们可以添加更多功能,例如数字输入来设置小时,但由于我们已经实现了所有操作符方法,我们可以认为PunchClock完成了。
尽管我们将在本书的其余部分讨论其他操作符,但这将是专门介绍它们的最后一章,因为我们已经学会了如何自定义它们执行的每一步。
摘要
我们已经深入了解了操作符如何在 Blender 的不同部分中集成,以及我们如何操纵它们的外观和行为。我们还学习了它们如何捕获输入以及它们如何与应用程序事件处理程序和界面交互。
本章标志着本书第二部分的结束。下一章,对象修饰符,是第三部分的开始,该部分讨论了场景数据如何被处理成最终输出。
问题
-
哪个操作符方法在操作符启动之前运行?
-
如果操作符的
poll方法返回False,用户能否启动操作符? -
我们能否在函数中为操作符参数设置默认值?
-
modal方法只能返回'RUNNING_MODAL'状态——是或否? -
返回
'CANCELLED'是否会撤销我们在方法中做的所有操作? -
我们能否在函数中为操作符参数设置默认值?
-
我们能否改变操作面板的布局?
第三部分:输出交付
本部分主要关注 3D 管道的最终阶段:生成和变形几何形状以及设置渲染和着色系统。探索简单机架的自动化和着色器节点树的构建。
本节包括以下章节:
-
第十一章,对象修饰符
-
第十二章,渲染和着色器
第十一章:对象修饰符
创建 3D 内容的主要部分包括通过添加和删除模型的顶点、边和面,或移动现有的面来编辑几何形状。
对象修饰符可以通过非破坏性编辑执行这些操作,这些编辑会影响对象的外观,但不会影响其内部数据。它们可用于生成建模,没有它们,动画将无法进行,因为变形对象需要改变每一帧的几何数据。
对象修饰符类似于在第八章中处理的 F-修饰符,但它们在目的和属性上具有更大的多样性。
在本章中,我们将涵盖以下内容:
-
理解和使用对象修饰符
-
脚本变形修饰符
-
生成骨架和网格
技术要求
在本章中,我们将使用 Blender 和 Visual Studio Code。本章创建的示例可以在以下 URL 找到:github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch11。
理解对象修饰符
对象修饰符改变对象的显示状态,而不改变其几何数据。我们在第八章中遇到了类似的情况,当时我们在不改变关键帧的情况下对动画 F-曲线应用了效果。
与 F-修饰符一样,它们可以堆叠在一起,并在 Python 中以集合属性的形式访问。
修饰符可以手动添加,但它们的创建和设置也可以通过脚本进行。在我们深入研究 API 之前,让我们看看如何在修饰符属性中创建它们。
添加修饰符
对象修饰符是在属性的修饰符选项卡中使用添加修饰符下拉按钮创建的。该选项卡带有扳手的图标。

图 11.1:在 Blender 中添加修饰符
点击添加修饰符显示可用选项。它们根据对象类型而变化:曲线没有像网格那么多的修饰符,而像空或相机这样的非几何类型根本不能有任何修饰符。
尽管随着时间的推移它们的数量有所增加,但所有修饰符都被分为四个类别:
-
修改 – 影响不会直接显示的数据,例如顶点组
-
生成 – 向或从对象添加或删除几何形状
-
变形 – 不添加或删除顶点、边或面,改变对象的形状
-
物理 – 将物理模拟的结果应用于对象

图 11.2:点击添加修饰符显示可用类型
界面显示每个类别一列,每个类别有四种修饰符类型。即使是同一类别的修饰符类型也彼此差异很大,并呈现不同的属性集。
为了更好地理解修改器的工作原理,我们可以在界面中创建一个修改器并查看它如何影响对象的几何形状。例如,向对象添加一个 细分曲面 修改器可以通过创建额外的多边形使其更加平滑。
细分对象
网格对象是多边形的;也就是说,它们由平面面和锐利的边缘组成。这对于像立方体这样的简单实体很好,但不适用于像球体或大多数现实生活中的物体这样的光滑表面。
为了产生平滑的错觉,我们细分网格的多边形,直到它们近似于连续的表面。
然而,过于密集的几何体是有问题的;它们需要更多的磁盘空间,并且不易于建模或编辑。因此,而不是存储额外的网格数据,我们使用 细分 曲面 修改器生成平滑的几何形状。

图 11.3:左侧的块状模型使用细分曲面进行了平滑
此修改器将每个边缘分成两个,从这些分割中生成新的多边形。默认情况下,一个名为 Catmull-Clark 的算法在保持整体形状的同时平滑结果。
我们可以通过以下步骤向模型添加细分:
-
打开 Blender 或通过 文件 | 新建 | 通用 返回默认场景。
-
选择默认的 Cube 形状并将其激活。
-
在 修改器 选项卡中,点击 添加修改器 并在 生成 列的底部选择 细分曲面。
-
在 修改器 属性中出现了新的条目。我们可以看到 细分 修改器的参数。

图 11.4:细分曲面属性
-
将
3中的数字增加会使我们的 Cube 看起来更像一个球体。 -
点击 简单 按钮禁用平滑:对象仍然细分,但其形状不会改变。
即使启用 简单 不会改变其形状,我们的对象仍然是细分的。这对于在 细分 之后添加的其他修改器可能很有用,因为它们将有更多的几何形状可以变形。
现在我们已经对 Cube 进行了细分,我们将能够使用第二个修改器:铸模 来改变其形状。
使用铸模改变对象的形状
在我们的 细分 修改器仍然在位的情况下,我们可以添加一个新的修改器来改变对象的形状:
-
在 修改器 选项卡中,点击 添加修改器 并在 变形 列的顶部选择 铸模。在 细分 下方出现另一个修改器。
-
修改
Cylinder和1.0。现在我们的几何体是一个圆柱体。
这两个修改器是重叠显示的。从顶部开始,每个修改器都作为下一个修改器的输入。因此,修改器列也被称为 修改器堆栈。
使用修改器,我们已经使我们的原始对象看起来像圆柱体,但这种变化可以被撤销,甚至可以动画化。移动0.0和1.0,我们的对象从其原始形状过渡到Cast中设置的形状。
我们可以使用 Blender 的 API 在 Python 脚本中重复前面的步骤。
在 Python 中添加修改器
Blender 对象的 Python 类包含一个modifiers属性。像所有集合一样,modifiers提供了new方法,该方法创建并返回新项。通过使用new,我们可以使用 Python 自动化修改器的设置。
查找集合类型项
Object.modifiers.new接受两个参数:name和type。第一个将在界面中的修改器属性中显示,而type指定我们想要创建哪种类型的修改器。type参数必须属于可用类型的列表,否则将导致错误。可用类型在 API 文档中列出:
docs.blender.org/api/3.3/bpy_types_enum_items/object_modifier_type_items.xhtml
但我们也可以从 Blender 本身获取它们。这些命令将在 Blender 的 Python 控制台中列出修改器关键字:
>>> import bpy
>>> mod_rna = bpy.types.ObjectModifiers.bl_rna
>>> mod_params = mod_rna.functions["new"].parameters
>>> mod_params["type"].enum_items.keys()
'DATA_TRANSFER', 'MESH_CACHE', 'MESH_SEQUENCE_CACHE', 'NORMAL_EDIT', 'WEIGHTED_NORMAL', 'UV_PROJECT', 'UV_WARP',
...
另一种获取修改器关键字的方法:
-
使用界面中的添加修改器按钮。
-
在Info日志中查找
modifier_add操作符的参数。它属于脚本工作区,正如我们从[第一章所知。

图 11.5:添加细分表面后的 Blender 信息日志
例如,'SUBSURF'的关键字。
使用modifiers.new
由于modifiers.new返回创建的修改器,我们可以存储返回值并重复之前理解对象 修改器部分的全部步骤。
我们将看到如何添加bpy并添加细分以增加可用的几何形状:
import bpy
ob = bpy.context.object
subdiv = ob.modifiers.new('Subdivision', 'SUBSURF')
subdiv变量包含新subdiv的属性。
如果我们在界面上看到一个属性的 Python 对应物,我们可以求助于Python 工具提示和开发者额外,这两个选项在编辑 > 首选项从顶部菜单。我们在第二章的有用的 Python 功能部分了解过它们。
如果启用了工具提示,将鼠标悬停在levels上。

图 11.6:在工具提示中显示的细分属性路径
为了在不改变其形状的情况下增加对象的顶点数,我们将subdiv.levels设置为3并将subdivision_type设置为'SIMPLE':
subdiv.levels = 3
subdiv.subdivision_type = 'SIMPLE'
现在我们有足够的多边形来变形我们的对象。我们添加一个'CAST'修改器,将其重塑为圆柱体:
cast = ob.modifiers.new('Cast', 'CAST')
cast.cast_type = 'CYLINDER'
cast.factor = 1.0
细分曲面和投射是自给自足的,因为它们不需要除了它们影响的对象之外的其他对象。其他修改器依赖于辅助对象的数据。
在下一节中,我们将设置一个依赖于变形器对象的修改器。
变形对象
许多变形器将一个对象的变化转换到另一个对象。这允许我们通过操作一个更简单的对象来变形一个复杂对象。以下是一些显著的例子:
-
曲线 – 沿着曲线对象变形网格
-
晶格 – 将常规网格的变化传递到网格
-
骨架 – 将一个可动结构的姿态传递到网格
-
表面变形 – 将一个网格的变形传递到另一个网格
可以使用modifier.object属性设置修改器使用的对象。
骨架修改器使用骨骼结构重现肢体运动,因此它需要一个可以摆姿势的特殊类型的对象。
另一方面,晶格修改器依赖于网格的内部坐标,这些坐标是特定于晶格对象类型的。
作为例子,我们将看到如何将晶格变形添加到对象中。
使用晶格修改器
要使用晶格变形,我们需要一个要变形的几何体和一个晶格类型的对象。我们可以使用 Blender 的吉祥物,猴子Suzanne:
-
打开 Blender 或通过文件 | 新建 | 通用返回默认场景。
-
通过按取消或X | 删除删除默认的立方体形状。
-
使用添加 | 网格 | 猴子将猴子头添加到场景中。
-
使用添加 | 晶格将晶格添加到场景中。
已将晶格添加到场景中。默认情况下,它比 Suzanne 小,因此我们需要将其放大:
-
在选择晶格后,按
S键进行缩放。拖动鼠标或按2键将其大小加倍。 -
选择 Suzanne 以添加修改器。在修改器选项卡中,使用添加修改器 | 晶格。
-
在晶格修改器中,单击对象属性并从选项中选择晶格对象。
编辑晶格对象会改变 Suzanne 的形状:
-
在3D 视图中选择晶格。
-
按下Tab键切换到编辑模式。
-
选择一个或多个晶格顶点。
-
按下G键并拖动鼠标以移动选择:对 Suzanne 应用晶格变形。

图 11.7:由晶格笼变形的网格
使用 Python 自动化这些步骤可以使过程更加简单。在下一节中,我们将编写一个一键设置晶格变形的操作符。
编写 Latte Express 附加组件
Latte Express附加组件在活动对象周围创建一个新的晶格并设置修改器。
它对于创建基本的卡通变形或风格化对象的骨架很有用。附加组件由一个操作符类和一个菜单项组成。
设置环境
我们为我们的附加组件创建一个 Python 脚本:
-
在
PythonScriptingBlender/ch11/addons文件夹中创建一个。我们可以使用文件管理器或我们的程序员编辑器的文件选项卡,例如,VS Code。 -
在该文件夹中创建一个新文件,并将其命名为
lattice_express.py。我们可以使用文件管理器或 IDE 中的新建文件按钮来完成此操作。 -
在您选择的编辑器中打开该文件。
-
在 Blender 的文件路径首选项中将
PythonScriptingBlender/ch11设置为,并重新启动 Blender。
现在我们可以编写插件并将其加载到 Blender 中。
编写 Latte Express 信息
如同其他插件一样,Latte Express 从一个空行开始,后面跟着bl_info字典:
bl_info = {
"name": "Latte Express",
"author": "Packt Man",
"version": (1, 0),
"blender": (3, 00, 0),
"description": "Create a Lattice on the active object",
"category": "Learning",
}
然后我们继续编写插件类和界面,在这种情况下,一个简单的操作符。
编写 Latte Express 操作符
我们需要导入bpy模块,这样我们才能创建一个新的操作符:
import bpy
class LatteExpress(bpy.types.Operator):
"""Set up Lattice Deformation"""
bl_idname = "object.latte_expresso"
bl_label = "Create Lattice on active object"
操作符需要一个活动对象,因此poll中的条件如下:
@classmethod
def poll(cls, context):
return context.active_object
创建晶格对象首先需要晶格数据,因此在execute函数内部,我们调用bpy.data.lattices.new并使用其返回值在bpy.data.objects.new中。即使这不是必需的,我们也按照活动对象命名新的对象和数据。最后,我们将晶格通过链接到当前集合添加到场景中:
def execute(self, context):
ob = context.object
latt_data = bpy.data.lattices.new(f"LAT-{ob.name}")
latt_obj = bpy.data.objects.new(
name=latt_data.name,
object_data=latt_data
)
context.collection.objects.link(latt_obj)
让我们调整晶格的大小,使其适合我们的对象。我们可以从dimensions属性中获取其大小:
latt_obj.scale = ob.dimensions
我们将晶格位置与活动几何中心匹配。我们可以从世界矩阵中获取对象位置,正如我们在第四章中学到的:
ob_translation = ob.matrix_world.to_translation()
仅此还不够,对于其变换枢轴远离几何中心的物体,这不会起作用,因此我们必须找到我们对象的实际中点。
寻找模型中心
要创建一个与对象位置和大小匹配的晶格,我们必须找到其顶点的中点,但不必查看所有组件。我们可以找到对象的边界框中心。
边界框是一个包含所有对象几何形状的想象中的平行六面体。我们可以通过在对象属性中激活边界来显示它。

图 11.8:带有坐标索引的对象边界框
在 Python 中,边界框是通过bound_box属性找到的。它是一个包含八个坐标的列表,我们可以通过插值两个对角顶点来找到中心。
箱子的中心位于其对角线的中间,因此我们想要在左下后角和右上前角之间进行调解。我们知道这些点的索引来自图 11。8*。它们分别是0和6。
找到这两个角落的一个更好的方法是使用min和max函数。
两个极端之一具有最低的x、y、z值,另一个具有最高的。换句话说,较低角落的分量具有最低的总和,而较高角落的分量具有最高的总和。
在 Python 中,min和max返回列表中的最小和最大值,但我们可以提供不同的标准,在这种情况下,使用sum,通过key参数:
btm_left = min((c for c in ob.bound_box), key=sum)
top_right = max((c for c in ob.bound_box), key=sum)
要找到两个向量之间的中点,我们可以使用Vector类中的线性插值方法(lerp)。在import部分,我们需要添加以下行:
from mathutils import Vector
然后,在execute中,我们插值两个坐标。lerp的第一个参数可以是任何三元组,所以我们只将两个角中的一个转换为Vector类型。因为我们正在寻找位于两个角之间中点的点,所以我们提供一个因子0.5作为第二个参数:
btm_left = Vector(btm_left)
ob_center = btm_left.lerp(top_right, 0.5)
我们将ob_center添加到ob_translation中,以便将晶格中心对齐到几何形状:
Ob_translation += ob_center
latt_obj.location = ob_translation
我们的中心和缩放后的晶格现在可以用来变形物体。我们可以在新的修改器中使用它,并返回{'FINISHED'}以退出操作:
mod = ob.modifiers.new("Lattice", 'LATTICE')
mod.object = latt_obj
return {'FINISHED'}
现在操作器已完成,我们可以添加一个菜单项来使用它。
添加创建晶格菜单项
我们定义一个函数将LatteExpress添加到菜单中。使用MOD_LATTICE:
def menu_func(self, context):
self.layout.operator(LatteExpress.bl_idname,
icon="MOD_LATTICE")
当然,我们需要注册新的 Blender 类和函数:
def register():
bpy.utils.register_class(LatteExpress)
ob_menu = bpy.types.VIEW3D_MT_object_context_menu
ob_menu.append(menu_func)
def unregister():
ob_menu = bpy.types.VIEW3D_MT_object_context_menu
ob_menu.remove(menu_func)
bpy.utils.unregister_class(LatteExpress)
我们可以刷新插件首选项并启用Latte Express。
使用 Latte Express 插件
一旦我们设置了ch11文件夹,我们就可以在Add-ons首选项中激活Latte Express。

图 11.9:启用 Latte Express 插件
我们的插件使得设置晶格变形的任务变得更加容易。让我们看看上一节,变形对象的工作流程是如何改进的:
-
通过File | New | General打开 Blender 或返回默认场景。
-
通过按Canc或X | Delete删除默认的Cube形状。
-
使用Add | Mesh | Monkey将猴子头添加到场景中。
-
使用鼠标右键在3D Viewport中打开对象菜单。
-
选择在活动对象上创建晶格。
晶格在活动物体上创建、居中并缩放,同时设置了一个修改器。除了自动化任务外,使用此插件还能提供更好的精度,因为晶格是根据物体的精确尺寸进行缩放的。
在下一节中,我们将添加更多对几何形状和晶格细分的控制。
改进 Latte Express 选项
一些物体可能没有足够的面数,使得晶格变形无法正常工作。我们在本章开头,在理解对象修改器部分遇到了类似的情况,当时我们在Cube形状上应用了一个Subdivision Surface修改器,然后才能使用Cast修改器将其重塑为圆柱体。
晶格物体的分辨率也可以增加,以获得对变形的更多控制。
由于这些原因,我们将向我们的操作员添加对象细分和晶格分辨率选项。
添加对象细分
我们向BoolProperty添加一个细分选项。由于我们还将设置SUBSURF修改器,我们添加IntProperty用于细分级别。
我们添加bl_options = {'REGISTER', 'UNDO'}以显示操作面板。LatteExpress的声明如下:
class LatteExpress(bpy.types.Operator):
"""Set up Lattice Deformation"""
bl_idname = "object.latte_expresso"
bl_label = "Create Lattice on active object"
bl_options = {'REGISTER', 'UNDO'}
add_subsurf: bpy.props.BoolProperty(default=True)
subd_levels: bpy.props.IntProperty(default=2)
execute方法考虑了这些选项,如果add_subsurf为True,则创建一个SUBSURF修改器:
def execute(self, context):
ob = context.object
if self.add_subsurf:
subdiv = ob.modifiers.new("Subdivision",
"SUBSURF")
subdiv.levels = self.subd_levels
subdiv.render_levels = self.subd_levels
subdiv.subdivision_type = "SIMPLE"
细分曲面有一个额外的属性用于渲染细分级别。我们将这两个值都设置为相同的值,以确保视口和渲染图像看起来相同。
改变晶格分辨率
晶格没有多边形或细分修改器,但它们有三个轴的分辨率参数,可以添加更多的细分。
points_u、points_v和points_w属性设置了其在x、y和z轴上的细分数量。
我们添加一个属性以影响网格分辨率。我们使用IntVectorProperty为这三个属性,并将其子类型设置为'XYZ',以便它们像坐标一样显示。分辨率坐标的最小值是1,而我们将默认值设置为3:
grid_levels: bpy.props.IntVectorProperty(
default=(3, 3, 3),
min=1,
subtype='XYZ'
)
由于晶格的实现方式,在创建晶格对象之前更改分辨率会改变晶格的起始尺寸。为了避免这种情况,我们只在latt_obj创建后设置points_u、points_v和points_w。
因此,晶格部分如下:
latt_data = bpy.data.lattices.new(f"LAT-{ob.name}")
latt_obj = bpy.data.objects.new(
name=latt_data.name,
object_data=latt_data
)
latt_data.points_u = self.grid_levels[0]
latt_data.points_v = self.grid_levels[1]
latt_data.points_w = self.grid_levels[2]
现在我们正在添加细分,晶格网格将呈现内部顶点,即最终位于晶格内部的控制点。我们不希望这样,因为我们使用晶格作为外部笼子。
因此,我们将晶格数据的use_outside设置为True:
latt_data.use_outside = False
之后,execute方法继续执行,将latt_obj链接到context.collection.objects,设置其位置和缩放,并在返回{'FINISHED'}之前创建对象修改器。
如果我们保存脚本并使用F3 -> 重新加载脚本并启动在活动对象上创建晶格,我们将看到晶格分辨率的选项。

图 11.10:使用 Latte Express 创建的 5x3x3 晶格
晶格网格添加快速变形,而无需在变形对象中添加额外的数据。例如骨骼这样的变形器,用于关节人物的修改器,需要分配顶点组才能正常工作。
使用轴网变形器
轴网是类似于晶格的变形对象,但它们不是使用网格,而是依赖于称为骨骼的子对象的平移、旋转和缩放,这与人类的骨骼相似。
默认情况下,骨骼以八面体棒的形式表示。轴网可以切换到姿态模式,这是一种特殊的 Blender 模式,其中可以使用在第七章中学到的技术单独动画化骨骼。
设置 骨架 修改器可能需要一些额外的步骤,但与用于网格的步骤类似。
将骨架对象添加到场景中
为了熟悉骨骼,我们将为 Suzanne 的几何形状创建一个简单的骨架:
-
打开 Blender 或通过 文件 | 新建 | 通用 返回默认场景。
-
通过按 取消 或 X | 删除 删除默认的 立方体 形状。
-
使用 添加 | 网格 | 猴子 将猴子头添加到场景中。
-
使用 添加 | 骨架 | 单个骨骼 将骨架添加到场景中。
到目前为止,我们应该在 Suzanne 的头上看到八面体骨骼的尖端。由于大部分骨骼都隐藏在模型内部,我们可以按 Z 键并切换到线框显示。

图 11.11:一个位于几何形状内部的骨架,带有线框显示
我们可以立即设置一个 骨架 修改器,但骨架通常不止一个骨骼。例如,我们可以向耳朵添加骨骼。
添加骨架骨骼
为了在创建新骨骼时获得更好的视图,我们可以按 1 键或从顶部菜单栏选择 视图 | 视点 | 前视图,然后按 . 键,或选择 视图 | 选择框,以将视图居中。
将骨架作为活动对象,我们通过按 Tab 键或使用左上角的下拉菜单切换到 编辑模式。然后,始终在前视图中,我们可以按照以下步骤向耳朵添加骨骼:
-
使用 Shift + A 组合键或从顶部菜单栏选择 添加 | 单个骨骼 来添加一个新的骨骼。新骨骼被添加到现有骨骼的上方。
-
点击骨骼以选择其中一个。
-
按下 R 键,然后输入
50,再按 Enter 键将骨骼向屏幕右侧旋转。 -
按 G 键,然后按 X 键水平移动它并输入
1。然后,按 Enter 键将骨骼移向 Suzanne 的左耳。 -
按 G 键,然后按 Z 键垂直移动骨骼并输入
0.2。然后,按 Enter 键将骨骼稍微向下移动。 -
Blender 为左右骨骼有一个命名约定。为了给左耳骨骼添加
.L后缀,我们从顶部菜单栏选择 骨架 | 名称 | 自动命名左右。 -
要为另一只耳朵创建骨骼,我们从顶部菜单栏选择 骨架 | 对称化。
生成的骨架应该类似于三叉戟。在这个例子中,骨骼的确切位置并不重要。

图 11.12:Blender 的 Suzanne 的耳骨
放置好三个骨骼后,我们可以通过按 Tab 键回到 对象模式,再按 Z 键回到 实体 视图。现在,我们可以将我们的几何形状绑定到骨架上。
将对象绑定到骨架上
如前所述,轴网变形器需要额外的信息:每个顶点都应该通过称为权重绘制的过程分配给一个或多个骨骼。对于绑定师来说这是一项手动任务,但我们可以使用 Blender 的自动权重来快速得到结果:
-
在对象模式中,在3D 视口中选择 Suzanne 对象,然后按住Shift键并选择轴网。
-
按Ctrl + P打开设置父对象菜单并选择使用自动权重。或者从顶部菜单选择对象 | 父对象 | 使用自动权重。
小心大纲视图
如果您使用大纲视图来选择对象,请注意其策略是不同的:
-
在视口中,多个对象中最后选择的对象是活动对象
-
在大纲视图中,多个对象中第一个选择的对象是活动对象
要将对象绑定到轴网上,我们可以在视口中先选择对象然后选择轴网,或者先在大纲视图中选择轴网然后选择对象。
现在我们可以通过摆动轴网骨骼来变形我们的网格:
-
选择轴网并按Ctrl + Tab,或者使用屏幕左上角的下拉框切换到姿态模式。
-
选择任何骨骼,然后使用G、R和S键移动、旋转或缩放它。

图 11.13:Blender 中的轴网变形
骨骼是变形模型的一种柔软、可控的方式。轴网网格也可以用轴网变形,因此我们可以在Latte Express中创建和设置轴网。
编写网格轴网脚本
轴网是 Blender 中动画推荐的途径,因为它们支持在不同.blend文件和其他高级动画功能之间进行链接。
将轴网绑定到轴网上可以使您在不切换到编辑模式编辑网格顶点的情况下进行变形动画。
添加轴网条件
我们希望轴网是一个可选功能,因此我们可以为它添加另一个属性。我们将其默认值设置为True,所以除非另行设置,否则将创建轴网:
add_armature: bpy.props.BoolProperty(default=True)
在execute方法内部,我们检查这个值并根据情况继续操作。
将轴网添加到场景中
轴网是以与轴网和其他对象相同的方式创建的:
-
创建新数据。
-
使用创建的数据创建新对象。
即使不是严格必需的,我们也将新轴网设置为轴网的父对象,以确保它们变换之间的一致性。
如果add_armature为False,我们立即设置轴网的位置。否则,我们创建一个新的轴网。底层代码在我们得到对象中点后不久接管:
# …
ob_translation = ob.matrix_world.to_translation()
ob_translation += ob_center
if not self.add_armature:
latt_obj.location = ob_translation
else:
arm_data = bpy.data.armatures.new(
f"ARM-{ob.name}"
)
arm_obj = bpy.data.objects.new(
name=arm_data.name,
object_data=arm_data
)
context.collection.objects.link(arm_obj)
一旦轴网成为场景的一部分,我们可以将其父对象设置为轴网并将其移动到对象所在的位置:
latt_obj.parent = arm_obj
arm_obj.location = ob_translation
通常情况下,骨架的变换枢轴位于受影响的几何体下方,因此当骨架处于其静止位置时,变形的角色将位于地面以上。因此,我们使用 dimensions 的第三个坐标将骨架移动到中心下方对象高度的一半:
half_height = ob.dimensions[2]/2
arm_obj.location[2] -= half_height
另一方面,晶格应该位于几何体中心,因此我们将其提升相同的量:
latt_obj.location[2] += half_height
现在骨架和晶格已经放置好了,我们需要创建一些骨骼。
创建编辑骨骼
为了手动创建骨骼,我们选择骨架并切换到 编辑模式。在 Python 中,执行相同的步骤如下:
context.view_layer.objects.active = arm_obj
bpy.ops.object.mode_set(mode='EDIT',
toggle=False)
我们将 False 传递给 mode_set 函数的 toggle 参数,因为我们不是在模式之间来回切换。
我们将添加与晶格垂直部分数量相同的骨骼。例如,具有 3 个垂直分辨率的晶格可以用三根骨骼绑定。

图 11.14:使用骨架骨骼绑定晶格
我们从 grid_levels 属性中获取级别的数量。每个骨骼从下部分开始,到下一部分结束。最后一根骨骼会超出晶格。
为了获得最佳长度,我们将对象高度除以内部骨骼的数量,即 grid_levels 减去一个骨骼:
grid_levels = self.grid_levels[2]
height = ob.dimensions[2]
bone_length = height / (grid_levels – 1)
我们使用一个 for 循环来添加一个 range 函数。我们给每个骨骼名称添加一个两位数的后缀:
for i in range(grid_levels):
eb = arm_data.edit_bones.new(f"LAT_{i:02}")
每个骨骼都有一个起点(头)和一个终点(尾)。由于骨架的起点与晶格的第一部分相匹配,第一根骨骼的坐标为 0, 0, 0。
第二根骨骼应该有更高的位置以留出前一根骨骼的长度,依此类推,因此每个骨骼头的表达式如下:
eb.head = (0, 0, i * bone_length)
骨骼的 尾 部分将 head[2] 坐标增加一个 bone_length:
eb.tail = (0, 0, eb.head[2] + bone_length)
为了将晶格顶点分配给骨骼,我们必须根据它们的 Z 坐标收集属于当前级别的顶点。
分配顶点到骨骼
对于每个晶格点,我们比较第三个坐标(co[2])与当前部分的相对高度。相对 意味着第一部分的高度为 0.0,最后一部分为 1.0,中间的部分为 0.5,依此类推。
Python 索引从 0 开始,所以最后一部分的索引是部分数量减 1。考虑到这一点,以下是获取每个级别的相对高度的方法:
rel_height = i / (grid_levels–- 1)
晶格点相对于中心,晶格的一边大小为 1.0,所以最低点的垂直坐标为 -0.5。因此,我们将 rel_height 降低 0.5 单位:
rel_height -= 0.5
为了分配顶点,我们需要一个包含它们索引的列表,我们将它存储在 vert_ids 列表中:
vert_ids = []
在这个列表中,我们需要存储晶格点的标识符编号,而不是它们的坐标。
晶格数据点是按顺序排列的,因此它们的标识符是它们的序号索引;也就是说,第一个点由索引1标识,第二个点有索引2,依此类推。
在 Python 中,我们可以使用enumerate来获取迭代项的序号:
for id, v in enumerate(latt_data.points):
if v.co[2] == rel_height:
vert_ids.append(id)
我们可以为每个骨骼创建一个以骨骼命名的顶点组,并使用add方法分配顶点。我们还提供一个1.0的权重,因为我们不是在两个组之间混合分配,并将'REPLACE'作为条件,因为我们不是从先前的分配中添加或减去:
vg = latt_obj.vertex_groups.new(
name=eb.name
)
vg.add(vert_ids, 1.0,'REPLACE')
创建骨骼并分配它们的影响是这个过程中的难点。现在我们可以创建修改器。
创建骨架修改器
我们通过modifiers.new方法向latt_obj添加一个新的骨架修改器,并使用arm_obj作为其变形对象:
arm_mod = latt_obj.modifiers.new("Armature",
"ARMATURE")
arm_mod.object = arm_obj
最后,我们保留'POSE',这样用户就可以进行动画制作:
bpy.ops.object.mode_set(mode='POSE',
toggle=False)
在这一点上,LattExpress一键创建晶格变形器和动画骨骼。作为一个可选步骤,我们可以为显示骨骼创建自定义形状。
添加自定义骨骼形状
骨架骨骼是有效的变形器,但在使用骨架变形器部分,我们为自己体验到了一个主要的缺点:骨骼往往被变形几何体隐藏。
有几种解决方案,例如在骨架属性中激活在前属性,并使用X-Ray或线框视图。
另一个技巧是通过将网格对象分配给骨骼的自定义 形状属性来显示特殊的小部件。
在 Python 中创建网格对象
首先,我们必须创建一个新的网格。一个网格由顶点坐标组成,以及连接顶点的边或面。

图 11.15:2D 正方形的顶点坐标
在这个例子中,我们创建一个线框正方形并将其用作骨骼小部件。Blender 骨骼在Y轴上扩展,所以放置垂直的骨骼的水平坐标是X和Z。
我们构建我们的顶点列表。我们希望边长为1.0,这是一个易于缩放的度量。因此,每一边将从-0.5到0.5的坐标,或者反过来。如果X和Z是第一个和最后一个坐标,这就是我们的顶点列表:
v_cos = [
[-0.5, 0.0, -0.5],
[-0.5, 0.0, 0.5],
[0.5, 0.0, 0.5],
[0.5, 0.0, -0.5]
]
接下来,我们需要一个边的列表。边是一对顶点索引,每个索引代表将要连接的两个顶点。正方形的四条边将顶点0连接到1,1连接到2,顶点2连接到3,3连接到0:
edges = [
[0, 1], [1, 2], [2, 3], [3, 0]
]
我们可以使用from_pydata方法从 Python 列表创建新的网格数据。由于我们不需要控制小部件中的面,第三个参数是一个空列表:
mesh = bpy.data.meshes.new("WDG-square")
mesh.from_pydata(coords, edges, [])
我们将网格添加到场景中:
wdg_obj = bpy.data.objects.new(mesh.name, mesh)
context.collection.objects.link(wdg_obj)
现在,我们可以将小部件形状分配给我们的骨架的姿态骨骼:
for pb in arm_obj.pose.bones:
pb.custom_shape = wdg_obj
以1.0的边长,我们的小部件也可以被隐藏,所以我们将其缩放以匹配dimensions对象。
考虑到骨骼的向上方向是 Y 轴,而 Blender 的向上方向是 Z,我们将 Z 自定义形状比例设置为 Y 维度:
pb_scale = pb.custom_shape_scale_xyz
pb_scale[0] = ob.dimensions[0]
pb_scale[2] = ob.dimensions[1]
Blender 通过骨骼的长度来缩放显示的自定义形状,所以我们通过骨骼长度来除以缩放:
pb_scale[0] /= bone_length
pb_scale[2] /= bone_length
我们脚本中创建的骨架已经准备好了。我们将整理场景并退出操作员。
存储骨骼对你的变量有害!
骨架骨骼由不同的 Python 实体表示,具体取决于当前模式。当骨架处于 EditBone 模式时,使用骨架数据的 edit_bones 集合。当骨架处于 PoseBone 模式时,使用 pose.bones 对象的 is。
这些集合每次骨架更新时都会重建,在它们改变时存储在变量中可能会导致崩溃。
完成设置
如果创建了骨架,我们将隐藏小部件网格和晶格:
wdg.hide_set(True)
latt_obj.hide_set(True)
execute 的最后步骤与之前相同:我们为变形对象创建晶格修改器,取消选择几何体,并完成:
mod = ob.modifiers.new("Lattice", "LATTICE")
mod.object = latt_obj
ob.select_set(False)
return {'FINISHED'}
重新加载脚本并启动 在活动对象上创建晶格 将创建一个完整的动画设置,包括骨架和骨骼形状。

图 11.16:通过动画控制变形的 Suzanne
此插件仍然可以改进。例如,我们可以为我们的动画控制构建椭圆形形状,或者通过将一些代码移动到特定函数中来整理 execute 方法,但既然它满足了其初始目的,我们可以认为它是完成的。
摘要
我们已经学习了如何使用修改器改变对象,以及如何将修改器绑定到动画对象。我们还对对象数据的工作原理以及不同类型的对象是如何创建、链接到场景和集成的有了更深入的了解。
使用这些知识,我们已经编写了一个可以用来变形任何对象的制作工具。
在本书的下一章和最后一章 第十二章,我们将探索 3D 管道的最后一步。
问题
-
修改器是否会改变对象的数据?
-
我们如何向对象添加修改器?
-
修改器是否依赖于除了它们变形之外的其他对象?
-
对或错:我们可以在创建晶格对象之前或之后更改晶格数据的分辨率,而不会产生任何后果。
-
我们如何在 Python 中向骨架添加骨骼?
-
对或错:只有一个骨骼集合属性。
-
对或错:只有网格类型的对象可以通过骨架变形。
-
Blender 有模式。当前模式是否会影响我们是否能在脚本中添加或删除数据?
-
我们如何使用 Python 创建网格?
第十二章:渲染和着色器
一个称为 渲染 的过程通过评估场景的几何形状、灯光和相机来生成完成图像的像素。
处理这些计算的渲染器可以是外部程序,独立于 3D 应用程序,或者动画包的完全集成功能。所有渲染器都有优点和缺点,可以分为两类:实时渲染,它通过假设一些近似值来实现即时可视化,和离线渲染,它需要更多时间来考虑更多细节。
为了生成图像,渲染器依赖于着色器——即关于物体如何对光线和观察者的位置做出反应,以及这如何转化为渲染像素的指令。
着色器可能很复杂,是一门独立的学科,但它们的工作基本概念并不难理解。
在本章中,你将学习如何设置渲染属性,如何使用 Python 自动创建着色器,以及如何使用 文件浏览器 加载图像。
本章涵盖了以下主题:
-
理解材料系统
-
在着色器编辑器中加载图像
-
连接和排列着色器节点
技术要求
本章我们将使用 Blender 和 Visual Studio Code。为本章创建的示例,以及媒体文件,可以在 github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch12 找到。
渲染和材质
Blender 随带两个渲染引擎:Eevee,一个可以在视口使用的实时渲染器,以及 Cycles,一个离线渲染器。其他引擎,包括大多数商业产品,可以以渲染插件的形式安装。还有一个选项,工作台,可以用于在视口中快速简单地显示渲染。
设置渲染引擎
当前 渲染引擎,以及其他渲染设置,可以在场景 渲染 属性中找到。它是第一个标签页,并带有电视机的图标:

图 12.1:选择当前渲染引擎
虽然 工作台 设计时只包含少量渲染选项和没有着色系统,Eevee 和 Cycles 可以通过基于节点的系统结合图像、颜色和属性。这可以在 着色器编辑器 区域完成,该区域位于 着色 工作区。
着色工作区
着色涉及不同的活动,如访问图像文件、检查视口和编辑物体属性。一旦我们将 渲染引擎 设置为 Eeeve、Cycles 或支持 Blender 着色系统的外部引擎,我们就可以在 着色 工作区执行这些任务。它包含以下内容:
-
用于导入图像的 文件浏览器
-
用于检查材质的 3D 视口 区域
-
一个 大纲
-
数据属性;默认情况下,世界设置选项卡是激活的
-
着色器****编辑器区域
-
图像****编辑器区域:

图 12.2:着色工作区
默认材质提供了一些可以在材质属性区域设置的着色属性。为了理解如何使用适当的布局编写可动材质的脚本,我们将简要概述着色器组件如何在着色器图中组合在一起。
理解物体材质
我们可以通过指定物体的材质来改变物体的整体外观。术语材质遵循现实世界物体的类比,其外观受其制成或涂层的材质影响。
物体的材质可以在材质属性区域中进行编辑。在 3.3 版本中,它是倒数第二个属性选项卡,带有带有棋盘图案的球体图标,如图图 12.3 所示。
设置材质属性
材质独立于物体存在。一种材质可以共享给多个物体,并且一个物体可以分配给不同面的集合的多个材质。
滚动到材质设置,我们可以编辑 Blender 如何处理材质——例如,其透明部分如何渲染在背景之上:

图 12.3:材质属性区域的设置
与实际材料通常关联的属性,如粗糙度、颜色和透明度,在表面面板中显示。这些属性是着色器的一部分,着色器是一种通用的算法,用于计算表面应该如何看起来。每种材料都与其关联一个着色器。
着色器为本书引入了一个新概念:称为节点树的可视框架。我们可以通过查看着色器****编辑器区域来了解它是如何工作的。
着色器编辑器
着色系统支持不同的风格:现实感、卡通或技术图纸等。而不是提供一个具有定义小部件的单个界面,渲染器的功能分散在称为节点的互联单元中。
就像函数一样,节点对一个或多个输入执行特定的操作,并通过一个或多个输出提供结果。节点是函数的可视表示,允许非程序员组合逻辑块以获得自定义结果。
节点不仅限于着色使用——它们还用于合成以及在几何****节点修改器中生成网格。
默认情况下,Blender 材质在着色器编辑器区域显示一个材质输出节点,其表面输入为原理 BSDF节点。双向散射分布函数(BSDF)是一个数学模型,描述了表面如何接收和反射光线。它是一种基于物理的渲染(PBR)形式,基于视觉属性如颜色、粗糙度和渗透性如何在现实世界中与光线相互作用。
材质输出是图中的最后一个节点,将着色传递到对象上。
任何时候只能有一个输出处于活动状态,因此着色器图也称为节点树,输出作为根节点,所有其他分支都由此节点衍生。
理解节点树
节点的输入和输出以彩色圆圈的形式显示,称为插座。输入插座位于节点的左侧,而输出插座位于右侧。它们的颜色取决于插座的数据类型。例如,原理节点的基础颜色插座,颜色为黄色,为材质分配颜色,而粗糙度,一个灰色插座,是一个浮点数,表示其与平滑度的距离。
紫色插座,如法线,是向量,可以包含方向数据。
输出插座位于节点的右侧,可以连接或链接到另一个节点的左侧输入插座:

图 12.4:一个接受颜色、粗糙度和法线输入的原理节点
因此,基础颜色的颜色属性是一个输入插座,可以连接到来自另一个节点的任何颜色输出。例如,原理节点的基础颜色输入可以来自RGB节点,如图12.4所示,也可以来自图像纹理,如图12.5所示:

图 12.5:一个原理节点,以图像作为基础颜色的输入
连接即转换!
我们可以将不同类型的插座连接起来,例如向量和颜色;数据会自动转换。X、Y和Z向量分量被转换为颜色的红色、绿色和蓝色元素,而颜色的亮度被转换为浮点值。
既然我们已经了解了材质的工作原理,我们将编写一个脚本,帮助加载图像纹理。
编写 Textament 插件
虽然创建着色器可能需要时间,但我们也可以自动化一些简单的操作。例如,我们可以编写一个插件来简化从磁盘加载图像并将它们连接到着色器的任务。
使用纹理图像
使用纹理图像节点,我们可以使用图像来为对象着色。这增加了材质外观的多样性,因为图像可以沿着对象的延伸方向变化,而不仅仅是单一颜色:

图 12.6:一个魔方的图像纹理,应用于一个平面立方体
我们将要编写的操作符将从磁盘加载多个图像,并从图像的文件名中猜测其用途。例如,名为 Metallic.png 的图像将被加载为纹理图像,并连接到Principled节点的Metallic输入。
与往常一样,我们将为开发新插件设置一个环境。
设置环境
我们将为我们的插件创建一个 Python 脚本,并通过执行以下步骤使其为 Blender 所知:
-
在名为
PythonScriptingBlender/ch12/addons的文件夹中创建一个文件夹。我们可以使用文件管理器或我们 IDE 的文件标签页来完成此操作,例如VS Code。 -
在该文件夹中创建一个新文件,并将其命名为
textament.py。我们可以使用文件管理器或我们 IDE 的新建文件按钮来完成此操作。 -
在您选择的编辑器中打开文件。
-
在 Blender 的文件 路径首选项中设置
PythonScriptingBlender/ch12。 -
重新启动 Blender 以更新搜索路径。
现在,我们将像往常一样开始编写插件信息。
编写 Textament 插件信息
在信息中,我们必须指定插件的作用以及其工具的位置:
bl_info = {
"name": "Textament",
"author": "Packt Man",
"version": (1, 0),
"blender": (3, 00, 0),
"description": "Load and connect node textures",
"location": "Node-Graph header",
"category": "Learning"
}
该插件仅包含一个类——一个用于加载图像的导入操作符。
编写导入操作符
我们的操作符从磁盘加载图像纹理,因此我们需要 os 模块来处理磁盘路径。除了 bpy,此操作符还将继承自 ImportHelper 工具类,以便它可以访问 Blender 的文件浏览器
import os
import bpy
from bpy_extras.io_utils import ImportHelper
从 ImportHelper 派生的操作符将所选文件路径存储在几个额外的属性中。
使用 Blender 的文件浏览器
与所有操作符一样,AddTextures 基于 bpy.types.Operator,但由于它操作文件,它也继承自 ImportHelper 类。通过继承这两个类,当它启动时,AddTextures 将运行 ImportHelper 的 invoke 方法,然后 execute 方法正常运行:
Class AddTextures(bpy.types.Operator, ImportHelper):
"""Load and connect material textures"""
bl_idname = "texture.textament_load"
bl_label = "Load and connect textures"
bl_description = "Load and connect material textures"
从 ImportHelper 派生为 AddTexture 添加了一个 filepath 属性,其中存储了所选文件的路径。
filepath 属性存储单个文件的磁盘路径,在我们的情况下这还不够,因为我们打算一次性加载多个文件。因此,我们需要将所选的 directory 存储在 StringProperty 中,并将所选的 files 作为 OperatorFileListElement 的集合:
directory: bpy.props.StringProperty()
files: bpy.props.CollectionProperty(
name="File Path",
type=bpy.types.OperatorFileListElement,
)
在另一个 StringProperty,filter_glob 中设置应显示在 .png 和 .jpg 图像中的文件扩展名。此属性是 "HIDDEN":我们不希望它在操作员的选项中显示:
filter_glob: bpy.props.StringProperty(
default="*.png; *.jpg",
options={"HIDDEN"})
现在,我们可以编写操作符方法。我们将从 poll 方法开始,该方法检查操作符是否可以启动。
检查活动节点的存在
此操作符在当前节点上工作,因此我们需要检查以下内容:
-
存在一个活动对象
-
存在一个活动的材质
-
已找到材质节点树
-
材料树有一个活动的节点
因此,poll方法在所有上述条件都适用的情况下才返回False:
@classmethod
def poll(cls, context):
ob = context.object
if not ob:
return False
mat = ob.active_material
if not ob:
return False
tree = mat.node_tree
if not tree:
return False
return tree.nodes.active
如果启动了操作符并且选择了文件,它们将被存储以在execute方法中使用。
匹配纹理文件名
在execute的开始,我们将当前活动的节点存储在一个变量中:
def execute(self, context):
mat = context.object.active_material
target_node = mat.node_tree.nodes.active
继承自ImportHelper的操作符在文件浏览器中的选择确认后显示execute方法。
因此,当execute运行时,self.files属性将包含用户选择的文件选择。我们可以迭代self.files,并将每个文件名与着色器输入进行比较。我们更愿意找到一个与文件名和输入名在大体上相似的文件名,而不是寻找完全匹配。
例如,"baseColor.png"应该连接到"Base Color"插座。换句话说,我们希望进行不区分大小写和不区分空格的匹配。
实现这一点的快捷方式是使用lower和replace方法的组合。我们可以在任何 Python 控制台中测试这一点;例如:
>>> "Base Color".lower().replace(" ", "")
'basecolor'
>>> "baseColor".lower().replace(" ", "")
'basecolor'
我们需要对每个文件、任何输入执行此操作,因此我们应该为这个操作创建一个函数。
lambda语句是一种通过仅声明其参数和一个表达式来创建函数的快捷方式。例如,将输入x转换为小写并作为一个无空格的字符串可以写成这种形式:
match_rule = lambda x : x.lower().replace(" ", "")
与def不同,lambda不将名称分配给函数,因为名称不是语法的要求:
lambda arguments : expression
由于我们将结果存储在match_rule变量中,我们的示例等价于编写以下几行:
def match_rule(x):
return x.lower().replace(" ", "")
lambda可以用来编写更紧凑的代码,或者如果需要一个函数作为参数但不需要直接调用它。
我们将对每个文件名和每个潜在的插座使用match_rule,并在寻找匹配项时比较结果。
节点插座存储在inputs中,这是每个节点的类似字典的集合属性。我们可以使用keys方法获取插座名称列表:
input_names = target_node.inputs.keys()
现在,是时候寻找可以链接的纹理了。我们可以组合两个 for 循环,遍历self.files中的每个条目的所有输入。如果找到匹配项,输入/文件名对将被添加到matching_names字典中:
matching_names = {}
for f in self.files:
for inp in input_names:
if match_rule(inp) in match_rule(f.name):
matching_names[inp] = f.name
break
break语句在找到匹配项时终止input_names循环,这样我们就可以继续处理下一个文件。
一旦matching_names字典包含了找到纹理的输入和相对文件名,我们就可以从磁盘加载图像并将它们添加到图中。
加载图像文件
self.file的元素不是完整的磁盘路径。我们可以从directory和os.path.join构建它,这样我们就可以使用bpy.data.images.load:
for inp, fname in matching_names.items():
img_path = os.path.join(self.directory, fname)
img = bpy.data.images.load(img_path,
check_existing=True)
check_existing参数避免了加载相同的图像多次:如果它已经在bpy.data.images中存在,则load方法返回现有条目。
我们提到,并非所有插座都是颜色,但也提到当它们连接时,向量、颜色和浮点插座会自动转换。因此,非颜色数据,如金属(一个浮点数)或法线(一个向量),可以存储在图像中。
节点图的一个主要观点是我们应该能够连接不同但大致相似类型的插座。
从图像获取非颜色属性
图像的颜色在信息方面并不与向量不同,它们由三个通道或组成部分组成:红色、绿色和蓝色。
将颜色插座连接到向量插座时,将分别使用红色、绿色和蓝色通道作为三维向量的X、Y和Z坐标。
如果颜色输出连接到浮点插座,则亮度,也称为其亮度或值,将用作浮点输入。
当图像用于存储值而不是颜色时,向 Blender 告知这一点很重要;否则,渲染器的颜色调整会改变图像信息。
如果一个插座不是"``RGBA"类型,我们可以通过将图像颜色空间设置为NonColor来做这件事:
if target_node.inputs[inp].type != "RGBA":
img.colorspace_settings.name = "Non-Color"
如果我们不这样做,即使是正确的纹理也会产生渲染伪影。
在这个阶段,图像已经加载到 Blender 中,但它们尚未出现在节点树中:我们需要为它创建一个图像纹理。
创建图像纹理节点
可以通过访问其node_tree的nodes集合来向材质着色图中添加新的材质节点。new集合方法需要节点为argument类型。在这种情况下,ShaderNodeTexImage是我们用于创建图像纹理的类型,但我们可以通过查看菜单提示来找到每个着色节点的 Python 类型。
如果在编辑 | 首选项中启用了Python 工具提示,就像我们在第二章中学到的那样,我们可以通过悬停在菜单项上查看节点类型:

图 12.7:悬停在添加菜单项上会在提示中显示节点类型
这样,我们可以创建一个新的纹理节点,并将其image属性设置为从磁盘加载的图像:
tree = mat.node_tree
tex_img = tree.nodes.new("ShaderNodeTexImage")
tex_img.image = img
添加到图中的纹理节点现在已准备好创建连接链接。
连接节点
虽然大多数纹理输出可以直接连接到着色节点,但某些输入类型可能需要在之间使用辅助节点。最突出的情况是细节,或法线贴图。在创建新连接之前,我们的代码应该检查是否需要额外的节点。
连接图像颜色
可以使用node_tree.links.new方法创建连接链接。它的参数如下:
-
出节点输出插座
-
接收节点的输入插座
如果我们不是在处理法线贴图,我们可以将纹理的 "Color" 输出连接到活动节点的输入。不需要其他操作,因此我们可以使用 continue 来传递到下一个输入:
if inp != "Normal":
tree.links.new(tex_img.outputs["Color"],
target_node.inputs[inp])
continue
# normal map code follows
一个 normal 输入不会触发 continue,因此我们不需要为它添加 else 语句:法线贴图代码随后,无需额外缩进。
连接法线贴图
仅使用几何形状渲染一个详细的表面将需要如此多的多边形,以至于生成的模型将过于庞大,无法存储或显示。
法线贴图使用 RGB 到 XYZ 转换在图像的像素中存储几何细节。
由于以这种方式存储的法线必须与原始法线合并,因此法线纹理不应直接连接到着色器节点;相反,它应通过一个 NormalMap 节点传递。
我们可以使用 new 在树中添加一个 "ShaderNodeNormalMap":
normal_map = tree.nodes.new(
"ShaderNodeNormalMap"
)
normal_map 的 "Normal" 输出可以通过以下代码连接到节点输入:
tree.links.new(normal_map.outputs["Normal"],
target_node.inputs[inp])
然后,我们必须将 tex_img 连接到 normal_map 节点:
tree.links.new(tex_img.outputs["Color"],
normal_map.inputs["Color"])
一旦 inp, fname 循环结束,我们可以返回 'FINISHED' 状态并退出:
return {'FINISHED'}
由于此脚本旨在使设置纹理更快,我们可以添加一个操作符按钮以快速执行。
添加标题按钮
我们在这本书的大部分操作符中使用了菜单,但这次,我们将在 Shader Editor 区域的顶部栏中添加一个按钮——即其标题栏。步骤与添加菜单项时使用的步骤相同:
-
创建一个接受两个参数
self和context的函数。 -
当插件注册时,将此函数附加到标题类型中。
使用 NODE_TEXTURE。layout.operator 方法将 AddTextures 显示为标题按钮:
def shader_header_button(self, context):
self.layout.operator(AddTextures.bl_idname,
icon="NODE_TEXTURE",
text="Load Textures")
现在,是时候注册操作符和标题函数了。我们可以通过查看 Blender 的源文件 space_node.py 来找到我们正在寻找的标题类 NODE_HT_header。可以通过右键单击并选择 Edit Source 将此文件加载到 Blender 的文本编辑器中。我们可以在 Shader Editor 区域的标题的任何元素上这样做:

图 12.8:NODE_HT_header 是 space_node.py 中的第一类
作为替代方案,我们可以使用 Blender 的 Python 控制台中的 comprehension 打印所有标题类型的列表。我们已经在 第八章 中学习了如何这样做:
>>> [c for c in dir(bpy.types) if
"header" in c]
['CLIP_HT_header', 'CONSOLE_HT_header', 'DOPESHEET_HT_header', 'FILEBROWSER_HT_header', 'GRAPH_HT_header', 'IMAGE_HT_header', 'IMAGE_HT_tool_header', 'INFO_HT_header', 'NLA_HT_header', 'NODE_HT_header', 'OUTLINER_HT_header', 'PROPERTIES_HT_header', 'SEQUENCER_HT_header', 'SEQUENCER_HT_tool_header', 'SPREADSHEET_HT_header', 'STATUSBAR_HT_header', 'TEXT_HT_header', 'USERPREF_HT_header', 'VIEW3D_HT_header', 'VIEW3D_HT_tool_header']
NODE_HT_header 在列表的中间。我们必须在 register 函数内部将其添加到我们的条目中:
def register():
bpy.utils.register_class(AddTextures)
bpy.types.NODE_HT_header.append(shader_header_button)
在 unregister 中,当插件禁用时,我们必须删除我们的界面和类:
def unregister():
bpy.types.NODE_HT_header.remove(shader_header_button)
bpy.utils.unregiser_class(AddTextures)
现在插件已经准备好了,我们可以用它来即时加载纹理。
使用加载纹理
如果 ch12 文件夹被添加到 Scripts 路径,我们可以在 Add-ons 预设的 Learning 类别中启用 Textament:

图 12.9:启用“Textament”插件
一旦启用,将在着色器 编辑器标题栏中添加一个名为加载纹理的按钮:

图 12.10:着色器编辑器标题栏中的加载纹理按钮
选择一个节点后,你可以点击加载纹理按钮,这将打开文件 浏览器区域。
要在简单模型上测试此插件,我们可以通过以下步骤将砖墙材质应用到默认立方体上:
-
打开 Blender 或通过文件 | 新建 | 通用返回默认场景。
-
使用窗口顶部的选项卡切换到着色工作区。
-
在着色器编辑器区域的标题栏中点击加载纹理。
-
在文件浏览器区域,导航到一个包含图像的文件夹。本章伴随的纹理可以在
github.com/PacktPublishing/Python-Scripting-in-Blender/tree/main/ch12/_media_/textures找到。 -
可选地,我们可以通过点击右上角的缩略图按钮将文件浏览器区域切换到缩略图模式。这对于寻找纹理很有用:

图 12.11:文件浏览器区域的“加载并连接”缩略图
-
我们可以通过套索、Ctrl + 点击或按A键来选择多个文件。
-
点击加载并连接将纹理添加到图中。
bricks_baseColor、bricks_normal和bricks_roughness纹理现在是材质的输入,使立方体看起来像砖墙:

图 12.12:砖纹理加载到着色器编辑器区域
操作员成功,但所有节点都创建在图的中心。我们可以通过添加重新排列节点的代码来显著改进这一点。
改进加载纹理
可以通过设置节点location属性的x和y属性来将节点移动到不同的位置。这允许我们在脚本中排列它们。
安排着色器节点
即使我们可以自由移动节点,API 也带来了一些限制:
-
我们无法访问插座的精确位置
-
新节点的高度和宽度在脚本中不可用
这两个问题中的任何一个都是可以容忍的,因为我们可以在节点输入的高度移动节点,或者实时获取新节点所需的空间。由于它们同时发生,我们将求助于一个解决方案。
假设节点间距
我们无法在脚本中获取新节点的大小,但我们可以通过查看现有的着色器树来预先了解纹理节点默认的大小。例如,在使用'Image' Texture'节点的dimensions`属性后:
>>> node_tree = C.object.active_material.node_tree
>>> node_tree.nodes['Image Texture'].dimensions
Vector((360.0, 410.0))
dimensions 返回的 Vector 属性包含节点的边界框,而不是节点的精确尺寸。我们可以通过查询节点的 width 来验证这一点:
>>> node_tree = C.object.active_material.node_tree
>>> node_tree.nodes['Image Texture'].width
240.0
即使存在,height 属性也没有帮助,因为它尚未更新,其值保持在 100.0 不变。
尽管 API 存在这种弱点,但我们有足够的信息来重新排列我们的树:在两个节点之间留下 100.0 单位的空间为连接留出足够的空间,因此我们可以在纹理和初始节点之间使用 340.0 单位的间距。
我们必须在操作符的声明中存储该值:
class AddTextures(bpy.types.Operator, ImportHelper):
"""Load and connect material textures"""
bl_idname = "texture.textament_load"
bl_label = "Load and connect textures"
bl_description = "Load and connect material textures"
_spacing = 340.0
为了垂直排列我们的节点,我们需要以正确的顺序处理它们。
节点创建排序
为了以正确的方式垂直排列节点,我们需要在处理它们的同时遵循目标节点布局中的插座顺序;否则,连接链接会相互交叉:

图 12.13:无序的垂直排列导致混乱、令人困惑的链接
Python 字典按设计是无序的,因此 matching_names 不遵循任何顺序,但 input_names 列表是按顺序排列的。通过使用 matching_names 过滤它,我们可以获得匹配输入的有序列表:
sorted_inputs = [
i for i in input_names if i in matching_names
]
我们必须将 for inp, fname in matching_names.items() 循环替换为 sorted_inputs 的迭代。由于我们需要一个序号来进行垂直间距,我们必须使用 enumerate 来获取当前输入的索引。以下是新的图像循环:
for i, inp in enumerate(sorted_inputs):
img_path = os.path.join(self.directory,
matching_names[inp])
img = bpy.data.images.load(img_path,
check_existing=True)
if target_node.inputs[inp].type != 'RGBA':
img.colorspace_settings.name = 'Non-Color'
img_node = mat.node_tree.nodes.new(
"ShaderNodeTexImage")
img_node.image = img
在连接纹理节点之后,我们可以更改其位置。我们首先使用与 target_node 相同的坐标,然后通过从 location.x 减去 _spacing 来将纹理向左移动:
img_node.location = target_node.location
img_node.location.x -= self._spacing
我们可以通过从 location.y 减去 self._spacing 来向下移动纹理节点。我们正在垂直堆叠节点,因此它们的 y 坐标取决于它们的序号索引。第一个节点(索引为 0)将完全保持在初始位置不动,第二个节点向下移动 self._spacing 倍的 1,第三个向下移动 self._spacing 倍的 2,依此类推:
img_node.location.y -= i * self._spacing
连接 ShaderNodeNormalMap 需要水平空间,因此,在我们将 normal_map 与其 img_node 对齐后,我们必须通过将纹理向左移动和将 normal_map 向右移动一半的 _spacing 来腾出一些空间:
normal_map = mat.node_tree.nodes.new(
"ShaderNodeNormalMap"
)
normal_map.location = img_node.location
img_node.location.x -= self._spacing / 2
normal_map.location.x += self._spacing / 2
现在,我们必须保存插件并更新它,通过点击 F3 并选择重新加载脚本。启动加载纹理设置了一个正确排列的节点树:

图 12.14:纹理设置,节点排列
现在基本功能已经完成,我们可以实现一个选项来自定义材质的颜色。
混合基础颜色
有时候,即使我们对纹理设置感到满意,我们仍然想要在保留纹理图案的同时更改颜色。我们可以通过在原则节点Base Color属性之前添加一个MixRGB节点来实现这一点:

图 12.15:使用 MixRGB 节点影响材质颜色
MixRGB节点具有一个用于混合两种颜色的因子滑块(Fac)。默认的混合类型Mix用Color2替换Color1,但计算机图形学中已知的其他混合模式,如Multiply、Overlay和Color Dodge,也是可用的。
ShaderNodeMixRGB节点的 Python 设置与"Base Color"类似,我们创建一个中间节点:
if inp == "Base Color":
mix = mat.node_tree.nodes.new(
"ShaderNodeMixRGB")
然后,我们将图像纹理和Mix节点对齐,并为额外的连接链接留出空间:
mix.location = img_node.location
img_node.location.x -= self._spacing / 2
mix.location.x += self._spacing / 2
我们将图像颜色连接到Mix节点的"Color1"输入:
mat.node_tree.links.new(
img_node.outputs["Color"],
mix.inputs["Color1"])
在这个阶段,我们应该连接img_node变量。
这样,通过连接所有其他输入(除了"Normal")的同一行代码,我们就可以将target_node连接起来:
img_node = mix
if inp != "Normal":
tree.links.new(tex_img.outputs["Color"],
target_node.inputs[inp])
continue
如果我们执行重新加载脚本然后再次启动加载纹理,就会创建一个类似于图 12.15中显示的混合布局。我们可以点击Color2属性并从拾色器中选择颜色,或者从下拉菜单中更改Mix节点的混合模式。
我们还可以尝试不同的解决方案。例如,我们可以使用"ShaderNodeMixRGB"与"ShaderNodeHueSaturation"以及"Color1"与"Color"。
节点树很有趣,因为它们可以被视为可视化编程,但即使是加载几个纹理这样的简单操作,如果手动进行,也可能需要花费时间。
这次,我们不需要为操作混合颜色创建界面,因为混合节点已经提供了它,所以我们能够结合两种过程方法的优点。
摘要
在本章中,我们学习了材质的工作原理以及如何在着色器编辑器区域中创建和连接节点。我们还学习了图像纹理如何改变着色物体的外观以及它们如何存储非颜色数据。
这是我们第一次接触节点树,这是一种通用的可视化编程方法,不仅限于着色器,并计划在未来扩展到变形和绑定。
基于节点的系统灵活且强大,但它们从脚本工具中受益,就像 Blender 的其他所有方面一样。
渲染并不是生产的最终步骤,因为合成和编辑在计算机图形管道中紧随其后。但既然这一阶段将三维数据转换为图像,它通常被认为是 3D 工作流程的最后一步。
这就结束了我们对 Blender 脚本工作原理的探索。我们已经涵盖了对象创建、变形、动画和渲染,但最重要的是,工具的设计和实现方式,以及如何克服软件的限制。
这些技能,结合个人才能和经验,使技术指导员能够在艺术需求与软件能力之间架起桥梁,赋能他们的团队,并在过程中提高他们的能力和理解。
问题
-
Blender 中存在多少渲染引擎?
-
“材质”和“着色器”这两个词有相同的意思吗?
-
着色器节点是预先定义的值,用于确定对象的外观,还是独立执行操作的单独单元?
-
我们能否使用图像为对象着色?
-
我们能否在不同数据类型之间建立联系?
-
我们如何在图中排列节点?
-
在我们的着色器中,我们能否改变来自图像的颜色?
附录
Blender 和 Python 如此庞大,以至于即使是针对短列表用例编写脚本也涵盖了广泛的技能和学科。本书包含了动画、绑定和着色元素,并在探索这些过程中介绍了编程技术。
本附录包含一个全面的总结,可以作为复习,帮助读者巩固本书探讨的概念,并帮助读者在章节之间导航。
第一部分:Python 简介
本节涵盖了脚本编写的基础知识,并帮助您熟悉 Blender 的 Python 实用工具。除了为后续章节提供坚实的基础外,它还包含了编写完整工作工具所需的所有信息。
第一章,Python 与 Blender 的集成
本章介绍了用于脚本编写的工具,内部和外部文本编辑器,以及版本控制。
这里总结了本章讨论的主题。
在主要操作系统上安装多个版本的 Blender
Blender 3.3 是写作过程中使用的长期支持版本。尽管本书的内容对所有 3.x 系列的 Blender 版本都有效,但如果您想在其他版本旁边安装 3.3 版本,以下提供了相应的说明:
-
使用 Windows 安装程序
-
使用 Microsoft Store
-
下载便携式存档
-
在 macOS 上安装
在 Blender 中使用 Python
脚本工作区是一个针对快速运行 Python 优化的 Blender 布局。它包括一个交互式控制台、一个列出过去操作命令的记录器,以及一个可以运行脚本的文本编辑器。我们将通过以下主题熟悉它:
-
使用“Hello World!”示例产生控制台输出
-
如何从信息日志中复制和粘贴 Python 指令
-
使用脚本检查 Blender 和 Python 的当前版本
-
函数和参数的解释
使用外部编辑器和版本控制工具
尽管文本编辑器快速且有用,但程序员通常还会利用外部代码编辑器。本书中使用的是来自Microsoft的多平台编辑器Visual Studio Code,但还有许多替代方案。版本控制工具是用于存储代码更改历史的实用工具。我们通过以下主题学习如何使用这些工具:
-
在 Visual Studio Code 中加载文件夹
-
在 Blender 文本编辑器中刷新文本文件
-
初始化和使用Git仓库
第二章,Python 实体和 API
本章解释了如何使用脚本与 Blender 交互,如何利用开发者功能,以及应用程序编程接口(API)的工作原理。
以下各节是本章讨论主题的摘要。
开发者用户界面功能
在 Blender 首选项的界面部分有两个有用的选项:
-
开发者附加功能:当我们在界面元素上右键单击时,它会显示编辑源选项,以便我们可以轻松访问用户界面(UI)的 Python 源代码。它还使非 UI 操作员在搜索栏中可用。
-
Python 提示:此功能显示鼠标光标下 UI 元素的相对 Python 属性。
开发者控制台功能
交互式控制台提供了两个方便的功能,用于快速脚本编写:
-
通过按Tab键进行代码自动补全
-
通过按上箭头键查看命令历史
开发者视图功能
包含在 Blender 中并提供在首选项 > 插件对话框中3D 视图部分的Math Vis (Console)插件显示三维数学实体,如向量和矩阵在 3D 视图中。当处理对象位置和旋转值时可能很有用。
使用 Blender 模块
在脚本中使用import语句访问 Blender 的 Python 模块bpy。它的每个组件都涵盖 3D 应用程序的特定方面。最值得注意的是,data包含当前会话中所有可用的对象,而context包含用户交互的当前状态,例如当前选择。API 文档可在网上查看,也可以使用help()函数查看。
使用对象集合
对象列表通过bpy_collection访问,这是一种类似于 Python dictionary的聚合类型。集合的元素可以通过数字索引或关键字访问,并且可以在 Python 循环中迭代。
像重命名这样的操作可以重新排序集合的元素,因此在顺序至关重要时建议转换为list。
Blender 集合没有append()方法:使用new()方法创建新对象,该对象将自动附加。remove()方法从集合中删除元素并将其从 Blender 中删除。
上下文和用户活动
用户可以通过添加或选择对象来更改 Blender 的当前状态或上下文。最后选中的对象被认为是活动的,并且是对象相关操作的主要目标。
上下文信息作为bpy.context的属性可用,是只读的,并且只能间接更改。例如,ob.select_set(True)用于选择一个对象,因为它不可能追加到bpy.context.selected_objects列表中。
第三章,创建您的附加功能
本章说明了创建 Blender 附加功能的过程:可以作为 Blender 插件安装的 Python 脚本,以添加自定义功能。
下面是本章讨论主题的总结。
编写附加功能的脚本
附加功能是包含名为bl_info的字典的 Python 模块或包。此字典包含有关附加功能的作者和名称等信息。附加功能必须提供两个函数,register()和unregister(),用于在启用或禁用附加功能时使用。
附加功能可以在 Blender 首选项中安装,但将它们开发的文件夹设置为F3键搜索栏中的重新加载脚本。
编写对象收集器,一个将对象分组在 Outliner 中的附加功能
向 Blender 添加功能涉及创建一个操作符,即可以从用户界面启动的指令。bl_idname和bl_label属性决定了操作符在 Blender 中的查找和显示方式,而poll()和execute()函数则规定了何时可以启动以及运行时会发生什么。
附加功能在其register()和unregister()函数中向 Blender 添加操作符。
处理附加功能的技巧
当使用外部编辑器时,启用自动保存可能有助于确保 Python 脚本始终包含最新的更改。
从开发文件夹启用附加功能可能会留下字节码,即名为__pycache__文件夹中的 Python 编译文件。如果我们使用 Git 版本控制,我们可以创建一个名为.gitignore的文本文件,其中包含__pycache__以避免字节码文件被版本化。
使用 try 和 except 避免重复
为了防止我们的脚本创建相同的集合两次,导致重复,我们在try语句中查找集合,并添加一个except KeyError块,当找不到集合时触发。通过在except语句下创建新的集合,我们确保具有给定名称的集合只创建一次。try/except 模式被称为宽恕而非许可,因为它侧重于从非允许的操作中撤回,而不是检查操作是否首先可行。
我们使用title()字符串方法为具有大写首字母的名称提供良好的格式化。我们可以创建函数将我们的运算符添加到 Blender 菜单中。它们接受self和context参数,并将运算符添加到self.layout。菜单函数通过附加组件的register()和unregister()函数添加到 Blender 中。
第四章,探索对象变换
本章展示了如何使用 Python 影响对象的location、rotation和scale,以及变换信息在 Blender 中的存储方式。
这里是本章讨论主题的总结。
使用 Python 移动和缩放对象
location存储为三维向量的x、y和z坐标。向量的坐标可以单独或一起更改,使用元组赋值。
scale也存储为x、y、z向量。虽然location的其余值具有(0.0, 0.0, 0.0)坐标,但未缩放对象的scale属性是(1.0, 1.0, 1.0)。
旋转的奇特之处
旋转比location和scale更复杂,因为三个轴上的旋转值可能会相互影响,导致一个称为万向节锁的问题。
旋转有多种表示方式;一些涉及多维实体,如四元数或旋转矩阵,以及两种角度度量单位:度和弧度。Blender 对象具有每个表示系统的属性,可以通过 Python 设置。提供了转换实用工具,用于在一种表示系统之间切换。
使用父亲和约束进行间接变换
对象可以按层次排列。层次中较高的对象(父对象)的变换会影响其下的所有对象(子对象)。
约束是另一种在不影响其通道的情况下转换对象的方法。它们可以通过使用constraints集合的new()方法添加。
使用矩阵变换对象
为location、rotation和scale通道设置值会影响对象的相对坐标。分配一个变换矩阵允许我们使用世界空间坐标。除非另有说明,矩阵值是延迟复制的;如果我们想存储一个矩阵作为变量并且不希望其值改变,我们需要使用它的copy()方法。
在 Python 中将对象作为变换对象进行父化会改变对象位置,除非在matrix_parent_inverse属性中设置了反向变换。
编写 Elevator,一个为所选对象设置楼层的附加组件
当启动时可以设置的FloatProperty运算符。可以通过切换其BoolProperty成员启用可选行为。
必须将可编辑属性作为注释、Python 任意属性添加。
在层次结构顶部移动父对象以避免重复变换。可以可选地使用约束。
第五章,设计图形界面
本章解释了如何添加自定义面板并将它们添加到 Blender 界面中。
下面是本章讨论主题的总结。
UI 组件
Blender 窗口结构为区域、区域和面板。面板使用 Python 填充文本、图标和按钮的布局。
编写 Simple Panel 插件
此插件注册了一个简单的Panel类,该类在row()或column()方法中显示文本和图标,使用split()显示非均匀列,使用grid_flow()显示均匀表格。
可以使用图标查看器插件或在某些情况下使用 Python 的字符串格式化查找 Blender 图标名称。
可以使用红色和灰色颜色通过小部件的alert或enabled标志提供视觉反馈。
使用operator()方法添加到布局中的操作员将显示为按钮。
第二部分:交互工具和动画
本节解释了如何将插件编写为文件夹而不是单个文件,与动画系统交互,并编写等待用户输入的模式操作员。在本节结束时,您将能够编写高级的交互式工具。
第六章,构建我们的代码和插件
本章解释了如何编写包含文件夹中多个文件的插件,并分发这些插件。
下面是本章讨论主题的总结。
模块、包和插件之间的关系
虽然单个.py文件是一个 Python 模块,但包含.py文件的文件夹是一个 Python 包。包包含一个名为__init__.py的文件。如果我们的包是插件,则此文件必须包含bl_info字典。
分区代码的指南
通过不同的.py文件分离代码的一些标准如下:
-
媒体加载器
-
通用代码与特定代码
-
界面代码
-
操作员模块
-
导入模块的使用
例如,所有用于加载自定义图标(如第五章中所述)的代码都可以移动到名为img_loader.py的模块中。
只有__init__.py文件会被importlib.reload()函数重新加载。
可以在preferences.py文件中编写用于显示插件首选项的面板,而panel.py和operators.py分别包含 UI 和插件操作员。
将压缩为.zip存档的插件文件夹可以使用首选项 | 插件 | 安装按钮安装。
第七章,动画系统
本章解释了如何在 Blender 中动画化对象,以及如何使用 Python 创建和编辑动画。
下面是本章讨论主题的总结。
动画系统
布局和动画工作空间在时间轴上显示动画关键帧,包括场景动作、关键帧和范围。关键帧确定了某个时间点的属性值。
编写 Action to Range 插件
此附加组件将播放的开始和结束设置为活动对象当前动作的第一帧和最后一帧。如果屏幕上显示时间轴,它将重新居中到新的范围。为此,使用context.temp_override()将时间轴区域传递给bpy.ops.action.view_all()工厂操作符。
编写 Vert Runner 附加组件
此附加组件沿着活动对象的顶点动画化所选对象。读取存储在context.object.data.vertices中的顶点坐标,同时使用三角函数计算将对象定向到其下一个位置的最短旋转弧。
第八章,动画修改器
本章介绍了用于动画 f 曲线的非破坏性修改器及其在动画程序效果中的应用。
下面是本章讨论主题的总结。
添加 f 曲线修改器
可以通过在图编辑器中选择曲线并从 f 曲线的modifiers集合中点击new()方法来添加 f 曲线修改器。
编写震动附加组件
此附加组件使用噪声f 修改器在活动对象上添加颤抖效果,并允许设置颤抖的持续时间和数量。软限制对噪声强度参数设置初始限制,同时仍然允许您使用键盘输入超出范围的值。我们添加了一个菜单项,通过在视图中右键单击菜单调用此操作符。
第九章,动画驱动器
本章介绍了动画驱动器,它们是用于控制复杂动作的不同属性之间的连接。驱动器可以在其逻辑中包含简短的 Python 表达式。
下面是本章讨论主题的总结。
创建和设置驱动器
可以通过从 Blender 属性的右键单击菜单中选择复制为新驱动器和粘贴驱动器来快速创建驱动器。使用对象的定位作为其旋转的输入创建了一个轮设置,因为当对象移动时,对象会旋转。
使用 Python 驱动器
通过在编辑 Blender 属性时按#键,然后输入 Python 代码,可以快速创建基于 Python 表达式的驱动器。可以使用三角周期函数如sin创建振荡运动,并将物理课堂中的摆动方程实现为驱动器表达式。可以将对象自定义属性用作驱动器表达式中的参数。
编写摆动附加组件
此附加组件立即设置摆动表达式和参数。通过使用object.driver_add()方法添加驱动器。
第十章,高级和模态算子
本章解释了如何通过丰富执行流程和实现可选方法来编写高级算子。
下面是本章讨论主题的总结。
算子执行细节
Operator 的 invoke() 方法,如果已定义,则在启动操作时运行。在 invoke() 内部,我们可以切换到 execute() 方法或 modal() 方法。后者监听用户输入,如按键或鼠标移动。
编写 PunchClock 扩展插件
此插件在场景中创建时间格式化的文本。其操作符在 invoke() 中使用 Python datetime 工具设置其小时和分钟参数的初始值。当将操作符添加到菜单时,布局的 operator_context 设置为 "INVOKE_DEFAULT",以确保 invoke() 的执行永远不会被跳过。
模态行为
操作符被添加到模态处理程序中,以便在 UI 的每次更新时运行其 modal() 方法。在模态内部,"MOUSEMOVE" 事件更改显示的小时和分钟。
自定义撤销面板
在通过实现 draw() 方法自定义执行后,撤销面板显示操作符属性。使用此方法,我们可以使用在 第五章 中学到的相同技术设计图形界面。
第三部分:输出交付
本节涵盖了 3D 管道的最终阶段:变形和渲染。
第十一章,对象修饰符
本章介绍了对象修饰符及其在动画中的应用。
下面是本章讨论主题的总结。
添加对象修饰符
修饰符分为四个类别:修改、生成、变形和物理。它们通过在 修饰符 属性中点击 添加修饰符 按钮来创建。
在 Python 中添加修饰符
object.modifiers 集合的 new() 方法需要一个 type 修饰符作为参数。可以通过访问 bpy.types.ObjectModifiers.bl_rna.functions["new"] 函数并查询其 parameters["type"].enum_items 来找到可能的 type 关键字列表。
编写 Latte Express 扩展插件
此插件设置一个 晶格 修饰符,使用三维网格笼变形对象。它通过查询其边界框找到模型的中心,并为改变晶格和对象的分辨率提供了输入参数。
使用骨架变形器
骨架通过变形骨架影响角色。在切换到 bpy.ops.object.mode_set() 后,可以使用 object.data.edit_bones.new() 在 Python 中创建骨骼。
在晶格对象上创建顶点组以将晶格顶点绑定到骨架骨骼。这样,可以通过脚本创建由骨架变形的晶格。
创建控制形状
用自定义线框形状替换默认的八面体形状使骨架更符合动画师的需求。因此,可以使用 mesh.from_pydata 方法在 Python 中创建一个简单的网格,并将其分配给 pose_bone.custom_shape 属性。
第十二章,渲染和着色器
本章介绍了渲染和材质、着色器编辑器及其节点树。尽管可能还有一些步骤,如后期处理和视频编码,但渲染通常被认为是 3D 流程的最后阶段。
下面是本章讨论主题的总结。
渲染的工作原理
像 Blender 的 Eevee 或 Cycles 这样的渲染引擎使用 着色器 将 3D 几何形状转换为完成的图像,以确定对象的外观。Blender 着色器是由称为节点的操作网络组成,即通过连接它们的输入/输出插孔来详细阐述和交换颜色和几何信息的块。
编写 Textament 插件
此插件从磁盘导入图像并创建 ImportHelper 和 Operator,并在调用时显示 Blender 文件浏览器。用户选择的文件作为 directory 和 files 成员属性访问。
在文件名中查找不区分大小写的匹配项
大写字母和空格可能会导致不希望的匹配错误,例如 "base color" 字符串没有与 "Base Color" 关联。可以使用常规语法编写字符串操作函数,或者使用一行 lambda 表达式定义。删除所有空格并将所有字母转换为小写的结果如下:
lambda x : x.lower().replace(" ", "")
图像中的非颜色数据
图像可以包含几何或遮罩信息。在这种情况下,必须将 colorspace_setting.name 图像属性设置为 "Non-Color",否则 Blender 将应用颜色过滤器,这会污染信息。
在着色器中连接图像
将 "ShaderNodeTexImage" 作为 node_tree.nodes.new("ShaderNodeTexImage") 的参数创建,允许你在着色器中使用图像。使用 node_tree.links.new() 创建纹理节点和着色器节点输入之间的连接。
法线贴图 纹理提供了细节的错觉。它们必须连接到 Normal Map,然后连接到着色器的 Normal 输入。
在标题中添加自定义按钮
可以像在菜单中添加操作员一样在标题中添加操作员:使用一个接受 self 和 context 参数的函数,并将元素添加到 self.layout。此函数附加到插件 register() 函数中的 标题类型。
在节点编辑器中排列节点
在 Python 中创建的节点位于编辑器的中心,并且相互重叠。可以通过设置它们的 location x 和 y 坐标来移动它们。它们应该放置在输出节点的左侧,并按照输出节点插孔的顺序垂直排列。
修改纹理颜色
可以通过在纹理和其输出节点之间添加一个 Mix 节点来操纵纹理的颜色。这允许你改变物体的整体颜色,同时保留来自图像的细节。


浙公网安备 33010602011771号