Blender-Python-API-教程-全-
Blender Python API 教程(全)
一、Blender 界面
本章讨论并定义 Blender 界面的组件。它作为我们在整个文本中讨论界面时使用的词汇的参考。我们将关注 Python 开发中最常用的接口组件,并为高效的 Python 脚本设置自定义接口。
为了避免在整本书中放置大的截图,我们严格定义了 Blender 界面中各种组件的名称。组件名称在这里以斜体介绍,并且在整个文本中第一个字符大写。
默认的 Blender 界面
当我们第一次打开 Blender 时,我们得到了熟悉的默认用户界面。我们在 3D 视口中的场景中绘制了一个立方体、一个相机对象和一个灯对象。图 1-1 是默认 Blender 界面的简单截图。图 1-2 显示了标注了各种主要部件的相同界面。我们讨论每个接口的功能。
图 1-2。
The components of the Default Blender interface
图 1-1。
The default Blender interface Note
为了便于打印,我们在 Blender 界面上应用了白橙色主题。默认的 Blender 主题是深灰色。
3D 视口
3D 视口,或简称为视口,为我们提供了工作产品的预览。当我们在 Blender 中操作数据时,3D 视口在更新自身之前等待所有进程完成写入数据。这在简单的操作中并不明显,比如平移和旋转,它们似乎是瞬间和实时发生的,但在插件开发中承认这一点仍然很重要。
3D 视口具有不同的查看选项和交互选项。查看选项包括实体、线框和渲染,而交互选项包括对象模式、编辑模式和雕刻模式。
标题菜单
标题菜单是图形用户界面的一个相当标准的标题。它允许我们在默认、动画和脚本等界面布局之间切换,以及在 Blender Render、Cycles Render 和 Blender Game 等渲染引擎之间切换。
属性窗口
“属性”窗口允许我们访问对象、场景、纹理、动画等的属性。“属性”窗口中的大多数界面会给出摘要和基本属性,而不是显示所有可用的详细信息。它对于跟踪现有对象、对象名称、已应用和未应用的转换以及其他一些重要属性非常有用。在 Blender artist 的布局中,此窗口通常总是打开的,因此它是放置附加功能的常用位置。
工具架和工具属性
工具架是按类型对不同类别的操作符进行分组的地方。如果我们展开窗口,我们可以看到工具架有各种选项卡,如工具、创建和关系。大多数 Blender 插件会在工具架上创建一个新的标签来保存它的操作符和参数。
工具属性窗口是一个动态窗口,Blender 根据用户激活的工具填充不同的参数集。例如,当使用旋转工具时,我们可以在此窗口中微调旋转,而不是导航到属性窗口中指定旋转的确切位置。工具属性是高级功能,通常旨在优化易用性,而不是为工具提供独特的功能。许多 Blender 插件完全忽略它们,只有少数原生 Blender 工具使用它们。
时间表
时间轴用于动画中。我们可以忽略这一点,因为我们不会在这本书的动画。
脚本接口
要进入脚本界面,请在标题菜单中帮助按钮右侧的下拉菜单中选择脚本选项。在整篇文章中,我们会用粗体的指令来呈现这样的指令,比如:标题菜单➤屏幕布局➤脚本。菜单的位置见图 1-3 。Blender 的布局将改变,如图 1-4 所示。
图 1-4。
The Scripting interface
图 1-3。
Selecting the Scripting interface
脚本布局,或者它的一些变体,将是我们在 Blender 中做大部分工作的地方。我们将讨论图 1-5 中介绍的 Blender 接口的新组件。
图 1-5。
Components of the Scripting interface
文字编辑器
我们可以在文本编辑器中编辑 Python 脚本(和任何其他文本文件)。我们可以分别单击“新建”和“打开”按钮来创建和打开脚本。一旦加载了脚本,文本编辑器底部的菜单栏将会改变,允许保存文件和在文件之间切换。
Blender 的文本编辑器有一些关于 Python 中的导入、系统路径和链接文件的特殊属性。我们将在本章的后面和以后的章节开发附加组件时详细讨论这一点。
命令日志
命令日志显示了 Blender 接口在会话期间进行的函数调用。在试验脚本和学习 API 时,这个窗口非常有用。例如,如果我们使用红色箭头在 3D 视口中平移立方体,我们会得到命令日志中清单 1-1 所示的输出。
bpy.ops.transform.translate(value=(3.05332, 0, 0), constraint_axis=(True, False, False),
constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1, release_confirm=True)
Listing 1-1.Command Log Output from Translation Along x-Axis
清单 1-1 中的输出显示我们从bpy.ops
子模块的transform
类中调用了translate()
函数。这些参数相当冗长,并且在从接口发出的调用中经常是多余的,但是它们足够简单,我们可以解释它们的意思并对函数进行实验。我们将在下一章深入研究这样的代码。虽然解密通常是学习 Blender Python 中函数的最好和最快的方法,但是我们也可以参考官方文档来获得更多细节。这也将在下一章讨论。
交互式控制台
交互式控制台是一个 Python 3 环境,类似于常见的 Python 控制台和 IPython 控制台,它们经常出现在 IDEs(交互式开发环境)的底部。交互控制台不与文本编辑器脚本共享本地或模块级数据,但是交互控制台和文本编辑器脚本都可以访问存储在bpy
及其子模块中的相同全局混合器数据。因此,控制台将不能读取或修改脚本本地的变量,但是对bpy
(以及一般的 Blender 会话)的修改是共享的。
更复杂的是,控制台和脚本在 Blender 会话期间共享链接的脚本和系统路径变量。这些组件之间的关系可能看起来不必要的复杂,但是我们将会看到它们的关系对于开发和实验都是最佳的。
自定义界面
Blender 界面的组件是模块化的、可拆卸的、可扩展的和全面可定制的。用户可以拖动任何窗口的右上角来修改和创建新窗口。
- 向左拖动右上角将创建一个相同类型的新窗口
- 向右拖动右上角将允许您超越相邻的窗口
- 按住 Shift 键并向任意方向拖动右上角将在一个新的分离窗口中复制组件
在一个可分离的窗口中创建一个 3D 视窗并复制文本编辑器是使用双屏设置的好方法。拥有两个可用的文本编辑器对于调试定制模块非常有帮助。参见图 1-6 的双屏设置截图。
图 1-6。
Example of a dual-screen development interface
请注意,如果在界面上移动时工具架或工具属性窗口消失,请在 3D 视口中按键盘上的 T 键来显示它们。此外,在 3D 视口中按键盘上的 N 键会显示一个新窗口,即对象属性。这个窗口在插件开发中经常使用,特别是当我们开始将自定义的 Blender 类作为参数分配给我们的对象时。
从命令行启动 Blender(用于调试)
在 Blender 中开发 Python 脚本时,我们从命令行启动 Blender 是非常重要的。当我们在 Blender 中运行脚本时,如果我们得到一个错误,命令日志将显示以下消息:
Python script fail, look in the console for now...
这条消息可能会非常混乱,因为交互式控制台将什么也不显示。Blender 的意思是:现在在终端中寻找…不幸的是,大多数人不通过终端打开 Blender,错误消息和回溯将不会被注意到,除非我们有一个在后台运行 Blender 的终端。通过终端打开 Blender 是 Python 开发者的非官方“调试模式”。Blender 有一个供核心开发者使用的官方调试模式,但这对于我们这些 API 用户来说一般没什么帮助。
要从终端打开 Blender,我们必须导航到保存在我们系统上的 Blender 发行版中的 Blender 可执行文件。确保已经下载了 Blender。来自 https://www.blender.org/download/
的适合操作系统的. zip 或. bz2 文件。将文件夹保存并解压缩到一个容易访问的位置。Windows 用户将打开命令提示符,UNIX 用户将打开终端。清单 1-2 和 1-3 分别显示了 Windows 和 UNIX 用户打开桌面上的 Blender 安装所需的命令。或者,Windows 用户可以正常打开 Blender,然后导航到标题菜单➤窗口➤切换系统控制台查看终端。
# Assuming you are starting from C:\Users\%USERNAME%
cd Desktop\blender-2.78c-windows64
blender
# Navigating from anywhere on the Windows
# filesystem to Blender on the Desktop
cd C:\Users\%USERNAME%\Desktop\blender-2.78c-windows64
blender
# If an existing Blender install causes
# the wrong version to open, use blender.exe
cd C:\Users\%USERNAME%\Desktop\blender-2.78c-windows64
blender.exe
Listing 1-2.Opening Blender from the Command Line in Windows
# Navigating to Blender on the Desktop from
# anywhere in the filesystem for Linux
cd ∼/Desktop/blender-2.78c-linux-glibc211-x86_64
./blender
# Navigating to Blender in the home directory for OSX
cd ∼/Desktop/blender-2.78c-OSX-10.6-x86_64
./blender
Listing 1-3.Opening Blender from the Command Line in UNIX
现在 Blender 正在从终端运行,它会将警告和错误转储到终端。如果我们退出终端,Blender 也会关闭。开发者应该总是从命令行打开 Blender 来获得详细的调试信息。我们通常会保持终端最小化,直到我们得到一个错误,然后最大化它来研究最近的输出。
运行我们的第一个 Python 脚本
有了本章介绍的信息,我们可以用命令行打开一个新的 Blender 会话,将界面安排成一个漂亮的开发布局,并准备好调试我们的 Python 代码。
我们的第一个目标是从立方体中创建一个立方体。我们将通过探索 Blender 和 API 的自然思维过程来创建实现我们目标的脚本。
寻找函数
首先,我们需要弄清楚哪个函数给场景添加了一个立方体。导航到三维视口,并转到三维视口标题➤添加➤网格➤立方体。现在导航到命令日志,验证该函数是否如清单 1-4 所示执行。
bpy.ops.mesh.primitive_cube_add(radius=1, view_align=False, enter_editmode=False,
location=(0, 0, 0), layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False))
Listing 1-4.Command Log Output for Adding a Cube to the Scene
测试功能
经过审查,我们看到许多论点,我们不需要完成我们的目标。我们不想进入编辑模式,我们不需要将 3D 视口与对象对齐,我们现在正在第一层工作。我们会猜测我们不需要参数view_align, enter_editmode
和layers
,并且它们的默认值是可以接受的。此外,我们将假设radius
指定立方体的大小,而location
指定位置。为了测试这一点,在交互控制台中运行清单 1-5 。
# Make a bigger cube sitting in the first quadrant
bpy.ops.mesh.primitive_cube_add(radius=3, location=(5, 5, 5))
Listing 1-5.Testing Defaults of primitive_cube_add()
通过在交互式控制台中运行清单 1-5 ,我们没有看到错误,并且我们在 3D 视口中看到一个以(5,5,5)为中心的大立方体。我们现在可以放心地在脚本中使用该函数来完成我们的目标,制作一个立方体的立方体。
从场景中删除我们的大立方体(和任何其他零散的物体),准备运行我们的脚本。在 3D 视口中使用 A 键切换全选,并按 X 键提示删除所有选定对象。
写剧本
确保到文本编辑器➤新创建一个新的脚本。为了创建一个立方体的立方体,我们将嵌套三个循环来迭代我们的 x、y 和 z 值。将清单 1-6 复制到文本编辑器中,并转到文本编辑器➤运行脚本。
import bpy
for k in range(5):
for j in range(5):
for i in range(5):
bpy.ops.mesh.primitive_cube_add(radius=0.25, location=(i, j, k))
Listing 1-6.Creating a Cube of Cubes
这个脚本创建一个 0.25 * 2 = 0.5 单位宽的立方体,以整数顶点的每个组合为中心,使得 0 ≤ x,y,z < 5. The result is pictured in Figure 1-7 。
图 1-7。
Cubes of cubes generated by Listing 1-6 Note
要查找函数、类、参数列表和最小文档,请使用 Blender 交互控制台的自动完成功能。将鼠标光标放在交互控制台的窗口上,开始输入一个bpy
功能。按 Ctrl+Space,Blender 会显示类和函数信息。
结论
在接下来的章节中,我们将详细介绍清单 1-6 的创建过程,让我们可以在 Blender 中创建任何东西。使用本章建立的词汇,我们将能够讨论 Blender Python 脚本中的高级概念。
二、bpy
模块
本章介绍并详述了bpy
模块的主要组件。这样,我们解释了 Blender 的许多重要行为。我们涵盖了选择和激活、创建和删除、场景管理和代码抽象。
Blender Python API 的官方文档可以在 http://www.blender.org/api/
选择 Blender 的一个版本找到。我们在本文中使用 Blender 2.78c,所以我们的文档可以在 http://www.blender.org/api/blender_python_api_2_78c_release/
找到。
模块概述
我们首先介绍一下bpy
每个子模块的背景。
bpy .运营
正如所暗示的,这个子模块包含操作符。这些主要是操纵对象的函数,类似于 Blender artists 在默认界面中操纵对象的方式。子模块还可以操作 3D 视口、渲染、文本等等。
对于操纵 3D 对象,两个最重要的类是bpy.ops.object
和bpy.ops.mesh
。object
类包含同时操作多个选定对象的函数以及许多通用工具。mesh
类包含一次操作一个对象的顶点、边和面的函数,通常是在编辑模式下。
目前在bpy.ops
子模块中有 71 个类,它们都有很好的名字和组织。
Note
模块、子模块和类的文档可以通过将 Pythonic 路径附加到对象并将.html
附加到您版本的 Blender 文档的主 URL 来直接访问。例如,bpy.ops.mesh
类的文档可以在这里找到: www.blender.org/api/blender_python_api_2_78c_release/bpy.ops.mesh.html
。
bpy .上下文
bpy.
context
子模块用于通过各种状态标准访问 Blender 的对象和区域。该子模块的主要功能是为 Python 开发人员提供一种访问用户正在使用的当前数据的方法。如果我们创建一个按钮来置换所有选中的对象,我们可以允许用户选择他所选择的对象,然后置换bpy.context.select_objects
中的所有对象。
我们在构建附加组件时经常使用bpy.context.scene
,因为它是某些 Blender 对象的必需输入。我们还可以使用bpy.context
来访问活动对象,在对象模式和编辑模式之间切换,并接受来自油性笔的数据。
bpy.data
该子模块用于访问 Blender 的内部数据。解释这个特定模块的文档可能会很困难(页面直接指向一个单独的类),但是我们在本文中将会非常依赖它。bpy.data.objects
类包含所有决定物体形状和位置的数据。当我们说前面的子模块bpy.context
可以很好地将我们指向对象组时,我们的意思是bpy.context
类将生成对bpy.data
类的数据块的引用。
bpy.app
这个子模块并没有完整的文档,但是到目前为止,我们确信这些信息可以在脚本和插件开发中发挥很大的作用。子模块bpy.app.handlers
是我们在本文中唯一关心的一个。handlers
子模块包含特殊功能,用于触发定制功能以响应 Blender 中的事件。最常用的是帧改变句柄,它在每次 3D 视口更新时(即帧改变后)执行一些功能。
bpy.types、bpy.utils 和 bpy.props
这些模块将在后面关于插件开发的章节中详细讨论。读者可能会发现*/bpy.types.html
中的文档对于描述我们在别处使用的对象类别很有用。
bpy.path
这个子模块本质上与 Python 自带的os.path
子模块相同。对于核心开发团队之外的 Blender Python 开发人员来说很少有用。
选择、激活和规范
Blender 界面设计得很直观,同时也提供了复杂的功能。某些操作逻辑上适用于单个对象,而其他操作逻辑上可以同时用于一个或多个对象。为了处理这些场景,Blender 开发人员创建了三种方法来访问对象及其数据。
- 选择:一次可以选择一个、多个或零个对象。使用选定对象的操作可以同时对单个对象或多个对象执行该操作。
- 激活:在任何给定时间,只有一个对象是活动的。作用于活动对象的操作通常更加具体和激烈,因此不能直观地一次对许多事物执行。
- 规范:(仅限 Python)Python 脚本可以通过名称访问对象,并直接写入其数据块。虽然操纵所选对象的操作通常是诸如平移、旋转或缩放之类的不同动作,但是向特定对象写入数据通常是诸如位置、方向或大小之类的声明性动作。
选择对象
在继续之前,建议读者在 3D 视口中创建一些不同的对象作为示例。转到三维视口标题➤添加查看对象创建菜单。
当我们在 3D 视口中单击鼠标右键时,对象会高亮显示和取消高亮显示。当我们按住 Shift 键并四处点击时,我们能够一次高亮显示多个对象。三维视口中的这些高光表示选定的对象。要列出所选对象,请在交互式控制台中键入清单 2-1 中的代码。
# Outputs bpy.data.objects datablocks
bpy.context.selected_objects
Listing 2-1.Getting a List of Selected Objects
正如我们前面提到的,bpy.context
子模块非常适合根据 Blender 中对象的状态获取对象列表。在本例中,我们提取了所有选定的对象。
# Example output of Listing 2.1, list of bpy.data.objects datablocks
[bpy.data.objects['Sphere'], bpy.data.objects['Circle'], bpy.data.objects['Cube']]
在这种情况下,名为Sphere
的球体、名为Circle
的圆和名为Cube
的立方体都在 3D 视口中被选中。我们返回了一个包含bpy.data.objects
数据块的 Python 列表。已知这种类型的所有数据块都有一个name
值,我们可以遍历清单 2-1 的结果来访问所选对象的名称。参见清单 2-2 ,这里我们获取了所选对象的名称和位置。
# Return the names of selected objects
[k.name for k in bpy.context.selected_objects]
# Return the locations of selected objects
# (location of origin assuming no pending transformations)
[k.location for k in bpy.context.selected_objects]
Listing 2-2.Getting a List of Selected Objects
现在我们知道了如何手动选择对象,我们需要根据一些标准自动选择对象。必备功能在bpy.ops
中。清单 2-3 创建一个函数,它将一个对象名作为参数并选择它,默认情况下清除所有其他选择。如果用户指定additive = True
,该功能将不会预先清除其他选择。
import bpy
def mySelector(objName, additive=False):
# By default, clear other selections
if not additive:
bpy.ops.object.select_all(action='DESELECT')
# Set the 'select' property of the datablock to True
bpy.data.objects[objName].select = True
# Select only 'Cube'
mySelector('Cube')
# Select 'Sphere', keeping other selections
mySelector('Sphere', additive=True)
# Translate selected objects 1 unit along the x-axis
bpy.ops.transform.translate(value=(1, 0, 0))
Listing 2-3.Programmatically Selecting Objects
Note
要在不使用 Python 脚本的情况下轻松查看对象名称,请导航到属性窗口并选择橙色立方体图标。现在,活动对象将在该子窗口的顶部附近显示其名称,如图 2-1 所示。此外,3D 视口的左下角将显示活动对象的名称。我们将在本章的下一小节讨论激活。
图 2-1。
Checking object names in the Blender interface
激活对象
与选择一样,激活也是 Blender 中的一种对象状态。与选择不同,在任何给定时间只能有一个对象处于活动状态。这种状态通常用于单个对象的顶点、边和面操作。这种状态与编辑模式也有密切关系,我们将在本章后面详细讨论。
当我们在 3D 视窗中左键单击时,我们单击的任何对象都将高亮显示。当我们以这种方式突出显示单个对象时,Blender 会选择并激活该对象。如果我们按住 Shift 并在 3D 视口周围单击鼠标左键,则只有我们单击的第一个对象是活动的。
注意图 2-1 中所示的属性窗口区域,其中显示了活动对象的名称。也可以通过图 2-1 底部的菜单激活对象。
要访问 Python 中的活动对象,在交互控制台中键入清单 2-4 。注意,有两个等价的bpy.context
类用于访问活动对象。就像选择的对象一样,我们返回一个bpy.data.objects
数据块,我们可以直接操作它。
# Returns bpy.data.objects datablock
bpy.context.object
# Longer synonym for the above line
bpy.context.active_object
# Accessing the 'name' and 'location' values of the datablock
bpy.context.object.name
bpy.context.object.location
Listing 2-4.Accessing the Active Object
列表 2-5 类似于列表 2-3 的激活。由于在任何给定时间只能有一个对象处于活动状态,因此激活功能要简单得多。我们将一个bpy.data.objects
数据块传递给一个场景属性,该属性在激活时处理内部数据。因为 Blender 只允许单个对象处于活动状态,所以我们可以对bpy.context.scene
进行单次赋值,并允许 Blender 的内部引擎处理其他对象的停用。
import bpy
def myActivator(objName):
# Pass bpy.data.objects datablock to scene class
bpy.context.scene.objects.active = bpy.data.objects[objName]
# Activate the object named 'Sphere'
myActivator('Sphere')
# Verify the 'Sphere' was activated
print("Active object:", bpy.context.object.name)
# Selected objects were unaffected
print("Selected objects:", bpy.context.selected_objects)
Listing 2-5.Programmatically Activating an Object
Note
当我们引入用于文本编辑器而不是交互式控制台(通常是多行程序)的清单时,我们总是导入bpy
。默认情况下,在交互控制台中导入bpy
模块,但是在文本编辑器中脚本的每次运行都是一个独立的会话,默认情况下不会导入bpy
。此外,当我们想在交互式控制台中查看程序的输出时,我们只需输入我们想查看信息的对象。当我们想要查看文本编辑器的输出时,我们使用打印函数将输出发送到打开 Blender 的终端。否则,除了文本编辑器脚本中的警告和错误,我们将看不到其他输出。
指定对象(按名称访问)
本节详细介绍了如何通过指定对象名来返回bpy.data.objects
数据块。清单 2-6 显示了如何访问给定名称的对象的bpy.data.objects
数据块。基于我们到目前为止的讨论,列出 2-6 可能看起来微不足道。数据块引用的这种循环性质有一个非常重要的目的。
# bpy.data.objects datablock for an object named 'Cube'
bpy.data.objects['Cube']
# bpy.data.objects datablock for an object named 'eyeballSphere'
bpy.data.objects['eyeballSphere']
Listing 2-6.Accessing an Object by Specification
清单 2-7 与清单 2-3 和 2-5 类似,但适用于规范。mySelector()
和myActivator()
的目标是返回具有给定状态的对象的数据块。在这种情况下,mySpecifier()
很容易返回数据块。
import bpy
def mySpecifier(objName):
# Return the datablock
return bpy.data.objects[objName]
# Store a reference to the datablock
myCube = mySpecifier('Cube')
# Output the location of the origin
print(myCube.location)
# Works exactly the same as above
myCube = bpy.data.objects['Cube']
print(myCube.location)
Listing 2-7.Programmatically Accessing an Object by Specification
伪循环引用和抽象
bpy.data.objects
数据块有一个非常有趣的属性,它突出了为 Blender Python API 做出的许多明智的架构决策。为了促进模块化、可扩展性和自由抽象,bpy.data.objects
数据块被构建为无限嵌套。我们称之为伪循环引用,因为虽然引用是循环的,但它们发生在对象内部而不是对象之间,这使得该概念不同于循环引用。
参见清单 2-8 中数据块进行伪循环引用的简单例子。
# Each line will return the same object type and memory address
bpy.data
bpy.data.objects.data
bpy.data.objects.data.objects.data
bpy.data.objects.data.objects.data.objects.data
# References to the same object can be made across datablock types
bpy.data.meshes.data
bpy.data.meshes.data.objects.data
bpy.data.meshes.data.objects.data.scenes.data.worlds.data.materials.data
# Different types of datablocks also nest
# Each of these lines returns the bpy.data.meshes datablock for 'Cube'
bpy.data.meshes['Cube']
bpy.data.objects['Cube'].data
bpy.data.objects['Cube'].data.vertices.data
bpy.data.objects['Cube'].data.vertices.data.edges.data.materials.data
Listing 2-8.Pseudo-Circular Referencing
清单 2-8 展示了 Blender Python API 的强大特性。当我们将.data
追加到一个对象时,它返回一个对父数据块的引用。这种行为有一些限制。例如,我们不能追加.data.data
来从bpy.data.meshes[]
数据块移动到bpy.data
数据块。尽管如此,这种行为将有助于我们构建清晰易读的自然模块化的代码库。
我们将在本文中创建工具,使我们能够在 Blender 中构建和操作对象,而无需直接调用bpy
模块。虽然伪循环引用看起来微不足道,就像我们在清单 2-8 中展示的那样,但是读者会发现在抽象bpy
模块时,它经常在工具箱中隐式地发生。
使用 bpy 进行转换
本节讨论了bpy.ops.transorm
类的主要组成部分及其在其他地方的类似物。它自然地扩展了抽象的主题,并介绍了一些有用的 Blender Python 技巧。
清单 2-9 是创建、选择和变换对象的最小工具集。脚本的底部运行一些示例转换。图 2-2 显示了 3D 视口中 minimal toolkit 测试运行的输出。
图 2-2。
Minimal toolkit test
import bpy
# Selecting objects by name
def select(objName):
bpy.ops.object.select_all(action='DESELECT')
bpy.data.objects[objName].select = True
# Activating objects by name
def activate(objName):
bpy.context.scene.objects.active = bpy.data.objects[objName]
class sel:
"""Function Class for operating on SELECTED objects"""
# Differential
def translate(v):
bpy.ops.transform.translate(
value=v, constraint_axis=(True, True, True))
# Differential
def scale(v):
bpy.ops.transform.resize(value=v, constraint_axis=(True, True, True))
# Differential
def rotate_x(v):
bpy.ops.transform.rotate(value=v, axis=(1, 0, 0))
# Differential
def rotate_y(v):
bpy.ops.transform.rotate(value=v, axis=(0, 1, 0))
# Differential
def rotate_z(v):
bpy.ops.transform.rotate(value=v, axis=(0, 0, 1))
class act:
"""Function Class for operating on ACTIVE objects"""
# Declarative
def location(v):
bpy.context.object.location = v
# Declarative
def scale(v):
bpy.context.object.scale = v
# Declarative
def rotation(v):
bpy.context.object.rotation_euler = v
# Rename the active object
def rename(objName):
bpy.context.object.name = objName
class spec:
"""Function Class for operating on SPECIFIED objects"""
# Declarative
def scale(objName, v):
bpy.data.objects[objName].scale = v
# Declarative
def location(objName, v):
bpy.data.objects[objName].location = v
# Declarative
def rotation(objName, v):
bpy.data.objects[objName].rotation_euler = v
class create:
"""Function Class for CREATING Objects"""
def cube(objName):
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0))
act.rename(objName)
def sphere(objName):
bpy.ops.mesh.primitive_uv_sphere_add(size=0.5, location=(0, 0, 0))
act.rename(objName)
def cone(objName):
bpy.ops.mesh.primitive_cone_add(radius1=0.5, location=(0, 0, 0))
act.rename(objName)
# Delete an object by name
def delete(objName):
select(objName)
bpy.ops.object.delete(use_global=False)
# Delete all
objects
def delete_all():
if(len(bpy.data.objects) != 0):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
if __name__ == "__main__":
# Create a cube
create.cube('PerfectCube')
# Differential transformations combine
sel.translate((0, 1, 2))
sel.scale((1, 1, 2))
sel.scale((0.5, 1, 1))
sel.rotate_x(3.1415 / 8)
sel.rotate_x(3.1415 / 7)
sel.rotate_z(3.1415 / 3)
# Create a cone
create.cone('PointyCone')
# Declarative transformations overwrite
act.location((-2, -2, 0))
spec.scale('PointyCone', (1.5, 2.5, 2))
# Create a Sphere
create.sphere('SmoothSphere')
# Declarative transformations overwrite
spec.location('SmoothSphere', (2, 0, 0))
act.rotation((0, 0, 3.1415 / 3))
act.scale((1, 3, 1))
Listing 2-9.Minimal Toolkit for Creation and Transformation (ut.py)
注意注释标签是有区别的和声明性的。在 Blender Python 中有几种旋转、缩放和平移对象的方法,但是记住哪些函数决定了一个表单(声明性的)以及哪些函数修改了一个表单(差分的)是很重要的。幸运的是,bpy
函数和类值的措辞相当直观。例如,rotate 是动词,因此是微分,而 rotation 是名词,因此是陈述性的。
清单 2-9 ,我们称之为ut.py
,是一个定制实用程序类的良好起点。
在这本书里,我们感兴趣的是教 Blender Python API,而不是作者的ut.py
模块。虽然ut.py
模块是一个很好的参考和教学工具,但我们将在以后的章节中避免使用它的单行函数调用。虽然这些函数调用可能在短期内解决我们的问题,但是它们模糊了我们想要通过重复来强化的类结构和参数。
现在,我们将在以后的章节中用ut.py.
做一些很酷的可视化,我们将添加庞大而有意义的实用函数,同时将单行函数视为占位符。
用最小工具包可视化多元数据
在本节中,我们使用清单 2-9 中的工具包来可视化多元数据。在我们开始之前,使用文本编辑器底部的工具条给这个工具包一个 Python 文件名ut.py
。现在,单击文本编辑器底部的加号创建一个新脚本。文件ut.py
现在是 Blender Python 环境中的一个链接脚本,我们可以将它导入到环境中的其他脚本中。
我们将可视化著名的费希尔虹膜数据集。这个数据集有五列数据。前四列是描述花的尺寸的数值,最后一列是描述花的类型的分类值。在这个数据集中有三种类型的花:setosa,versicolor 和 virginica。
清单 2-10 作为这个例子的标题代码。它导入必要的模块:我们的工具包ut
、csv
模块和urllib.request
。我们将使用urllib
从文件库中获取数据,然后使用csv
解析它。没有必要理解清单 2-10 中的所有代码来从这个例子中获益。
import ut
import csv
import urllib.request
###################
# Reading in Data #
###################
# Read iris.csv from file repository
url_str = 'http://blender.chrisconlan.com/iris.csv'
iris_csv = urllib.request.urlopen(url_str)
iris_ob = csv.reader(iris_csv.read().decode('utf-8').splitlines())
# Store header as list, and data as list of lists
iris_header = []
iris_data = []
for v in iris_ob:
if not iris_header:
iris_header = v
else:
v = [float(v[0]),
float(v[1]),
float(v[2]),
float(v[3]),
str(v[4])]
iris_data.append(v)
Listing 2-10.Reading in iris.csv for the Exercise
可视化三维数据
由于 Blender 是一个 3D 建模套件,可视化三维数据似乎是最合理的。清单 2-11 在 3D 视口的(x,y,z)值处放置一个球体,该视口由每次观察的萼片长度、萼片宽度和花瓣长度指定。
# Columns:
# 'Sepal.Length', 'Sepal.Width',
# 'Petal.Length', 'Petal.Width', 'Species'
# Visualize 3 dimensions
# Sepal.Length, Sepal.Width, and 'Petal.Length'
# Clear scene
ut.delete_all()
# Place data
for i in range(0, len(iris_data)):
ut.create.sphere('row-' + str(i))
v = iris_data[i]
ut.act.scale((0.25, 0.25, 0.25))
ut.act.location((v[0], v[1], v[2]))
Listing 2-11.Visualizing Three Dimensions
of Data
球体的结果集出现在 3D 视口中,如图 2-3 所示。显然,本文中印刷的 2D 图片并没有很好地体现这一模式。使用 Blender 的鼠标和键盘移动工具,用户可以非常直观地探索这些数据。
图 2-3。
Visualizing Three Dimensions of Iris Data
可视化四维数据
幸运的是,使用 Blender Python,我们有三种以上的方法可以参数化对象。为了说明最后的数字变量,花瓣宽度,我们将通过花瓣宽度来缩放球体。这将允许我们在 Blender 中可视化和理解四维数据。清单 2-12 是对之前版本的一个小小的修改。
# Columns:
# 'Sepal.Length', 'Sepal.Width',
# 'Petal.Length', 'Petal.Width', 'Species'
# Visualize 4 dimensions
# Sepal.Length, Sepal.Width, 'Petal.Length',
# and scale the object by a factor of 'Petal.Width'
# Clear scene
ut.delete_all()
# Place data
for i in range(0, len(iris_data)):
ut.create.sphere('row-' + str(i))
v = iris_data[i]
scale_factor = 0.2
ut.act.scale((v[3] * scale_factor,) * 3)
ut.act.location((v[0], v[1], v[2]))
Listing 2-12.Visualizing Four Dimensions of Data
球体的结果集出现在 3D 视口中,如图 2-4 所示。很明显,下面一组球体的萼片宽度很小。图 2-5 放大这组数据。
图 2-5。
Visualizing Four Dimensions of Iris Data Pt. 2
图 2-4。
Visualizing Four Dimensions of Iris Data
可视化五维数据
从我们到目前为止所看到的,在这个数据中至少存在两个非常不同的集群。我们将挖掘花卉物种数据来寻找关系。为了在 3D 视口中轻松区分不同类型的花,我们可以为每种花指定一个几何形状。参见清单 2-13 。
# Columns:
# 'Sepal.Length', 'Sepal.Width',
# 'Petal.Length', 'Petal.Width', 'Species'
# Visualize 5 dimensions
# Sepal.Length, Sepal.Width, 'Petal.Length',
# and scale the object by a factor of 'Petal.Width'
# setosa = sphere, versicolor = cube, virginica = cone
# Clear scene
ut.delete_all()
# Place data
for i in range(0, len(iris_data)):
v = iris_data[i]
if v[4] == 'setosa':
ut.create.sphere('setosa-' + str(i))
if v[4] == 'versicolor':
ut.create.cube('versicolor-' + str(i))
if v[4] == 'virginica':
ut.create.cone('virginica-' + str(i))
scale_factor = 0.2
ut.act.scale((v[3] * scale_factor,) * 3)
ut.act.location((v[0], v[1], v[2]))
Listing 2-13.Visualizing Five Dimensions of Data
3D 视口中的结果输出(图 2-6 )揭示了数据中尺寸和物种之间的关系。我们看到许多球果,在较大的簇的顶端是海滨锦葵花,我们看到许多立方体,在较大的簇的底部是杂色花。这两个物种的尺寸有些重叠。球体,刚毛花,组成了完全分离的较小尺寸的花簇。
图 2-6。
Visualizing Five Dimensions of Iris Data
讨论
用不到 200 行代码,我们为交互式多元数据可视化软件构建了一个强大的概念验证。像这样的概念可以用我们尚未涉及的高级 API 函数来扩展,包括纹理、GUI 开发和顶点级操作。目前,我们的示例软件可以在以下方面进行改进:
-
在我们的五维可视化中,改变球体的颜色比给每个物种指定一个形状更直观。
-
我们读入数据的方法是静态的和无 GUI 的。一个插件开发者自然希望将这种方法应用于任何数据集,让用户全面控制他查看什么以及如何查看。
-
无法缩放可视化工具的数据。iris 数据工作得很好,因为数值方便地在(0,0,0) 10 的范围内,这大约是默认情况下容易查看的 Blender 单元的数量。
-
我们可以研究一个更好的系统来缩放对象,使它们最好地代表数据。比如球体的体积与半径的立方成正比,那么我们可以考虑将数据值的立方根作为半径传递给
scale()
函数。可以说这创造了一个更直观的形象。由于 3D 视口中球体覆盖的面积与其半径的平方成比例,因此可以对数据值的平方根进行同样的论证。
注意,通过ut.py
,主脚本能够在 Blender 中操纵模型,而无需调用或导入bpy
。这无论如何都不是一个推荐的做法,但是它是 Blender Python 环境如何将bpy
视为函数和数据的全局集合的范例。
结论
本章介绍了很多关于 Blender Python API 的重要高级概念,以及bpy
模块的详细核心函数。在下一章,我们将详细讨论编辑模式和bmesh
模块。在第三章结束时,用户应该能够使用 API 创建任何形状。随着我们引入更加复杂和相互依赖的过程,抽象将变得更加重要和费力。
三、bmesh
模块
到目前为止,我们已经讨论了创建、管理和转换整个对象的方法。Blender 的默认模式是对象模式,它允许我们选择和操作一个或多个对象,通常使用可以适当应用于不同对象组的变换,如旋转和平移。
当我们进入编辑模式时,Blender 开始作为一个 3D 艺术套件发光。此模式允许我们选择单个对象的一个或多个顶点来执行高级和详细的变换。正如所料,大多数用于编辑模式的操作不能在对象模式下执行,反之亦然。
模块几乎只处理编辑模式的操作。因此,在深入研究bmesh
的功能之前,我们将适当处理对象模式和编辑模式之间的差异。
编辑方式
要像传统的 Blender 3D 艺术家一样手动进入编辑模式,请转到 3D 视口标题➤交互模式菜单➤编辑模式,如图 3-1 所示。使用同一菜单切换回对象模式。
图 3-1。
Toggling Between Edit and Object Mode
当切换到编辑模式时,此时激活的对象将是用户在该编辑模式会话中唯一可以编辑的对象。如果用户想在编辑模式下操作不同的对象,他必须先切换回对象模式来激活所需的对象。只有这样,在切换回编辑模式并激活所需对象后,他才能操作它。如果此时关于选择和激活的措辞不清楚,请参考第二章中的“选择、激活和规范”一节。记住,我们总是可以在交互控制台中运行bpy.context.object
来检查被激活对象的名称。
要以编程方式在对象模式和编辑模式之间切换,请使用清单 3-1 中的两个命令。
# Set mode to Edit Mode
bpy.ops.object.mode_set(mode="EDIT")
# Set mode to Object Mode
bpy.ops.object.mode_set(mode="OBJECT")
Listing 3-1.Switching Between Object and Edit Mode
选择顶点、边和平面
要开始处理单个对象的细节,我们必须能够选择特定的部分。我们将在ut.py
模块中包装我们的模式设置函数,然后讨论如何使用bmesh
来选择对象的特定部分。这样做,我们将解决bmesh
和 Blender 中顶点索引协议的一些怪癖和版本兼容性缺陷。
在编辑和对象模式之间持续切换
清单 3-2 实现了一个在对象模式和编辑模式之间切换的包装函数。我们将在第二章开始构建的ut.py
工具包中插入这个。我们对普通bpy.ops
方法所做的唯一修改是,当我们进入编辑模式时,取消选择活动对象的所有顶点、边和平面。目前,Blender 用于确定对象的哪些部分在进入编辑模式时被选择的协议是不透明的和不实用的。我们将采取最安全和最一致的方法,每当我们进入编辑模式时,取消选择对象的每个部分。
当我们从编辑模式进入对象模式时,Blender 只是恢复我们第一次进入编辑模式时的活动和选定对象。这种行为是可靠的,也是可以理解的,所以我们不会修改bpy.ops.object.mode_set(mode = "OBJECT")
的标准行为。
# Place in ut.py
# Function for entering Edit Mode with no vertices selected,
# or entering Object Mode with no additional processes
def mode(mode_name):
bpy.ops.object.mode_set(mode=mode_name)
if mode_name == "EDIT":
bpy.ops.mesh.select_all(action="DESELECT")
Listing 3-2.Wrapper Function for Switching Between Object and Edit Mode
Note
如果你在同一个 Blender 会话中多次编辑像ut.py
这样的定制模块,确保调用模块上的importlib.reload(ut)
来查看将未缓存的版本导入 Blender。参见清单 3-3 中的示例。
# Will use the cached version of ut.py from
# your first import of the Blender session
import ut ut.create.cube('myCube')
# Will reload the module from the live script of ut.py
# and create a new cached version for the session
import importlib importlib.reload(ut) ut.create.cube('myCube')
# This is what the header of your main script
# should look like when editing custom modules
import ut
import importlib importlib.reload(ut)
# Code using ut.py ...
Listing 3-3.Editing Custom Modules, Live Within a Blender Session
实例化 bmesh 对象
在 Blender 中,与其他核心数据结构相比,bmesh
对象相当笨重,计算量也很大。为了保持效率,Blender 将大部分数据和实例管理工作交给用户通过 API 进行管理。在我们探索bmesh
模块时,我们将继续看到这样的例子。参见清单 3-4 中实例化bmesh
对象的例子。一般来说,实例化一个bmesh
对象需要我们在编辑模式下将一个bpy.data.meshes
数据块传递给bmesh.from_edit_mesh()
。
import bpy import bmesh
# Must start in object mode
# Script will fail if scene is empty
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create a cube and enter Edit Mode
bpy.ops.mesh.primitive_cube_add(radius=1, location=(0, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')
# Store a reference to the mesh datablock
mesh_datablock = bpy.context.object.data
# Create the bmesh object (named bm) to operate on
bm = bmesh.from_edit_mesh(mesh_datablock)
# Print the bmesh
object
print(bm)
Listing 3-4.Instantiating a bmesh Object
如果我们尝试在交互式控制台中运行这些命令,我们可能会得到不同的结果。bmesh
对象的实例不是持久的。除非 Blender 检测到它正在被使用,否则bmesh
对象将解引用网格数据块,垃圾收集内部数据,并返回<BMesh dead at some_memory_address>
。考虑到维护一个 bmesh 对象所需的空间和计算能力,这是一种可取的行为,但它确实需要程序员执行额外的命令来保持它的活力。我们在构建选择 3D 对象特定部分的函数时会遇到这些命令。
选择 3D 对象的部分
为了选择一个bmesh
对象的部分,我们操作每个BMesh.verts
、BMesh.edges
和BMesh.faces
对象的选择布尔。清单 3-5 给出了一个选择立方体各部分的例子。
注意清单 3-5 中对ensure_lookup_table()
的多次调用。我们使用这些函数来提醒 Blender 在操作之间保持BMesh
对象的某些部分不被垃圾收集。这些函数占用最小的处理能力,所以我们可以随意调用它们,而不会产生太大的影响。多调用它们比少调用它们好,因为调试此错误:
ReferenceError: BMesh data of type BMesh has been removed
在没有ensure_lookup_table()
协议的大型代码库中可能是噩梦。
import bpy
import bmesh
# Must start in object mode
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create a cube and enter Edit Mode
bpy.ops.mesh.primitive_cube_add(radius=1, location=(0, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')
# Set to "Face Mode" for easier visualization
bpy.ops.mesh.select_mode(type = "FACE")
# Register bmesh object and select various parts
bm = bmesh.from_edit_mesh(bpy.context.object.data)
# Deselect all verts, edges, faces
bpy.ops.mesh.select_all(action="DESELECT")
# Select a face
bm.faces.ensure_lookup_table()
bm.faces[0].select = True
# Select an edge
bm.edges.ensure_lookup_table()
bm.edges[7].select = True
# Select a vertex
bm.verts.ensure_lookup_table()
bm.verts[5].select = True
Listing 3-5.Selecting Parts of 3D Objects
读者会注意到我们运行bpy.ops.mesh.select_mode(type = "FACE")
。这个概念到目前为止还没有涉及到,但是理解它对于正确使用高级编辑模式功能是很重要的。通常,Blender artists 在 3D Viewport Header 中点击三个选项中的一个,如图 3-2 所示。图 3-2 中的按钮对应于bpy.ops.mesh.select_mode()
中的垂直、边缘和面参数。现在,这只会影响我们在编辑模式下可视化选择的方式。我们在这个例子中选择 FACE,因为它是同时可视化所有三种类型的最佳模式。在本章的后面,我们将讨论编辑模式中的一些功能,它们的行为将根据选择而改变。
图 3-2。
Toggling various selection modes
编辑模式转换
本节讨论简单变换,如编辑模式下的平移和旋转,以及高级变换,如随机化、挤出和细分。
基本转换
非常方便的是,我们可以使用在第二章中用于对象模式转换的相同函数来操作 3D 对象的各个部分。我们将给出一些使用清单 2-9 中介绍的bpy.ops
子模块列出 3-6 的例子。轻微变形立方体的输出见图 3-3 。
图 3-3。
Deforming cubes with edit mode operations
import bpy
import bmesh
# Must start in object mode
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create a cube and rotate a face around the y-axis
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(-3, 0, 0)) bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")
# Set to face mode for transformations
bpy.ops.mesh.select_mode(type = "FACE")
bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.faces.ensure_lookup_table()
bm.faces[1].select = True
bpy.ops.transform.rotate(value = 0.3, axis = (0, 1, 0))
bpy.ops.object.mode_set(mode='OBJECT')
# Create a cube and pull an edge along the y-axis
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0)) bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")
bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.edges.ensure_lookup_table()
bm.edges[4].select = True
bpy.ops.transform.translate(value = (0, 0.5, 0))
bpy.ops.object.mode_set(mode='OBJECT')
# Create a cube and pull a vertex 1 unit
# along the y and z axes
# Create a cube and pull an edge along the y-axis
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(3, 0, 0)) bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")
bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.verts.ensure_lookup_table()
bm.verts[3].select = True
bpy.ops.transform.translate(value = (0, 1, 1))
bpy.ops.object.mode_set(mode='OBJECT')
Listing 3-6.Basic Transformations in Edit Mode
高级转换
我们不可能涵盖 Blender 中用于编辑网格的所有工具,所以我们将在这一节中涵盖一小部分,并在本章末尾列出更多使用示例。清单 3-7 实现了挤压、细分和随机化操作符。预期输出见图 3-4 。
图 3-4。
Extrude, Subdivide, and Randomize Operators
import bpy import bmesh
# Will fail if scene is empty
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create a cube and extrude the top face away from it bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(-3, 0, 0)) bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")
# Set to face mode for transformations
bpy.ops.mesh.select_mode(type = "FACE")
bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.faces.ensure_lookup_table()
bm.faces[5].select = True
bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate =
{"value": (0.3, 0.3, 0.3),
"constraint_axis": (True, True, True),
"constraint_orientation" :'NORMAL'})
bpy.ops.object.mode_set(mode='OBJECT')
# Create a cube and subdivide the top face
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")
bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.faces.ensure_lookup_table()
bm.faces[5].select = True
bpy.ops.mesh.subdivide(number_cuts = 1)
bpy.ops.mesh.select_all(action="DESELECT")
bm.faces.ensure_lookup_table()
bm.faces[5].select = True
bm.faces[7].select = True
bpy.ops.transform.translate(value = (0, 0, 0.5))
bpy.ops.object.mode_set(mode='OBJECT')
# Create a cube and add a random offset to each vertex
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(3, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.transform.vertex_random(offset = 0.5)
bpy.ops.object.mode_set(mode='OBJECT')
Listing 3-7.Extrude, Subdivide, and Randomize Operators
关于索引和交叉兼容性的说明
读者可能已经注意到,3D 对象中的顶点、边和面的索引没有特定的顺序排列。在迄今为止的示例脚本中,作者已经提前手动定位了索引,而不是以编程方式发现它们。例如,当操作清单 3-7 中的立方体顶部时,作者预先确定ut.act.select_face(bm, 5)
将选择立方体顶部的面。这是通过反复试验确定的。
使用试错法测试来发现一个对象的一部分的索引号通常是一种可接受的实践,但是有许多缺点。在任何给定的 Blender 版本中,索引语义应该被认为是可复制的,但不可管理的。
-
不同版本的 Blender 的默认对象索引差别很大。作者指出了依赖于不同版本 Blender 的硬编码索引的附加组件的主要兼容性问题。版本 2.77 和版本 2.78 之间的主要区别在于依赖于硬编码索引的附加组件。
-
Behavior of indexing after certain transformations is very unwieldy. See Figure 3-5 for an example of the vertex indices of a default plane, a plane after three insets, and a plain after two subdivisions. The indices in these planes conform to no particular logical pattern. Variance among transformations is another source of cross-version incompatibility.
图 3-5。
Default, inset, and subdivided planes with vertex indices labeled
-
使用硬编码索引的附加组件在用户交互的可能性方面非常有限。使用硬编码索引的插件可以连续运行,但很少会与用户来回交互。
解决这个问题的方法是根据特征进行选择。为了通过特征选择顶点,我们循环遍历对象中的每个顶点,并在满足标准的顶点上运行bm.verts[i].select = True
。这同样适用于边和面。理论上,这种方法看起来计算量很大,算法也很复杂,但是你会发现它惊人的快和模块化。根据特性使用纯选择的插件通常可以同时在 Blender 的许多版本上成功运行。不幸的是,实现这一点在 Blender 中打开了一个关于局部和全局坐标系的概念上的麻烦。我们将在下一节中对此进行阐述。
全局和局部坐标
Blender 为每个对象的每个部分存储许多组坐标数据。在大多数情况下,我们将只关心两组坐标:全局坐标 G 和局部坐标 l。当我们对对象执行变换时,Blender 将这些变换存储为变换矩阵 t 的一部分。Blender 将在某些时候将变换矩阵应用于局部坐标。Blender 应用变换矩阵后,局部坐标将等于全局坐标,变换矩阵将是单位矩阵。
在 3D 视口中,我们总是看到全局坐标 G = T * L。
我们可以用bpy.ops.object.transform_apply()
控制 Blender 何时应用变换。这不会改变对象的外观,而是将 L 设置为等于 G,将 T 设置为等于单位。
我们可以利用这一点来轻松选择对象的特定部分。如果我们通过不运行和不退出编辑模式来延迟bpy.ops.object.transform_apply()
的执行,我们可以维护两个数据集 G 和 L。在实践中,G 对于相对于其他对象定位对象非常有用,而 L 非常容易循环读取索引。
查看清单 3-8 获取对象的全局和局部坐标的函数。给定bpy.data.meshes[].vertices
数据块为v
,v.co
给出本地坐标和 bpy。data.objects[].matrix_world * v.co
给出全局坐标。幸运的是,这个数据块可以在对象模式和编辑模式下访问。我们将构建独立于模式的函数来访问这些坐标。参见清单 3-8 获取独立于模式的每组坐标的函数。
这些函数牺牲了一些清晰度来换取简洁和高效。在这段代码中,v
是表示我们的矩阵 L 的元组列表,obj.matrix_world
是表示我们的变换矩阵 t 的 Python 矩阵。
def coords(objName, space='GLOBAL'):
# Store reference to the bpy.data.objects datablock
obj = bpy.data.objects[objName]
# Store reference to bpy.data.objects[].meshes datablock
if obj.mode == 'EDIT':
v = bmesh.from_edit_mesh(obj.data).verts
elif obj.mode == 'OBJECT':
v = obj.data.vertices
if space == 'GLOBAL':
# Return T * L as list of tuples
return [(obj.matrix_world * v.co).to_tuple() for v in v]
elif space == 'LOCAL':
# Return L as list of tuples
return [v.co.to_tuple() for v in v]
class sel:
# Add this to the ut.sel class, for use in object mode
def transform_apply():
bpy.ops.object.transform_apply(
location=True, rotation=True, scale=True)
Listing 3-8.Fetching Global and Local Coordinates
参见清单 3-9 中本地和全球坐标行为的示例。我们打印转换前、转换后和transform_apply()
后立方体的前两个坐标三元组。这在纸上和代码编辑器中都是有意义的。在交互控制台中逐行运行清单 3-9 突出了transform_apply()
的有趣行为。平移立方体后,读者将看到立方体移动,但局部坐标保持不变。运行transform_apply()
后,立方体不会移动,但是局部坐标会更新以匹配全局坐标。
import ut
import importlib
importlib.reload(ut)
import bpy
# Will fail if scene is empty
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0))
bpy.context.object.name = 'Cube-1'
# Check global and local coordinates
print('\nBefore transform:')
print('Global:', ut.coords('Cube-1', 'GLOBAL')[0:2])
print('Local: ', ut.coords('Cube-1', 'LOCAL')[0:2])
# Translate it along x = y = z
# See the cube move in the 3D viewport
bpy.ops.transform.translate(value = (3, 3, 3))
# Check global and local coordinates
print('\nAfter transform, unapplied:')
print('Global: ', ut.coords('Cube-1', 'GLOBAL')[0:2])
print('Local: ', ut.coords('Cube-1', 'LOCAL')[0:2])
# Apply transformation
# Nothing changes in 3D viewport
ut.sel.transform_apply()
# Check global and local coordinates
print('\nAfter transform, applied:')
print('Global: ', ut.coords('Cube-1', 'GLOBAL')[0:2])
print('Local: ', ut.coords('Cube-1', 'LOCAL')[0:2])
############################ Output ###########################
# Before transform:
# Global: [(-0.5, -0.5, -0.5), (-0.5, -0.5, 0.5)]
# Local: [(-0.5, -0.5, -0.5), (-0.5, -0.5, 0.5)]
#
# After transform, unapplied:
# Global: [(2.5, 2.5, 2.5), (2.5, 2.5, 3.5)]
# Local: [(-0.5, -0.5, -0.5), (-0.5, -0.5, 0.5)]
#
# After transform, applied:
# Global: [(2.5, 2.5, 2.5), (2.5, 2.5, 3.5)]
# Local: [(2.5, 2.5, 2.5), (2.5, 2.5, 3.5)]
###############################################################
Listing 3-9.Behavior of Global and Local Coordinates and Transform Apply
在下一节中,我们将使用这个概念来解决图 3-5 中出现的问题,并释放 Blender 中编辑模式的全部力量。
按位置选择顶点、边和面
参见清单 3-10 中的两个函数,这两个函数协同工作,便于根据顶点、边和面在全局和局部坐标系中的位置进行选择。我们指定为ut.act.select_by_loc()
的函数看起来非常复杂,但是没有使用我们到目前为止还没有引入的任何 Blender 概念。作者认为这个函数应该作为bmesh
模块的一部分,因为它的应用非常广泛。
# Add in body of script, outside any class declarations
def in_bbox(lbound, ubound, v, buffer=0.0001):
return lbound[0] - buffer <= v[0] <= ubound[0] + buffer and \
lbound[1] - buffer <= v[1] <= ubound[1] + buffer and \
lbound[2] - buffer <= v[2] <= ubound[2] + buffer
class act:
# Add to ut.act class
def select_by_loc(lbound=(0, 0, 0), ubound=(0, 0, 0),
select_mode='VERT', coords='GLOBAL'):
# Set selection mode, VERT, EDGE, or FACE
selection_mode(select_mode)
# Grab the transformation matrix
world = bpy.context.object.matrix_world
# Instantiate a bmesh object and ensure lookup table
# Running bm.faces.ensure_lookup_table() works for all parts
bm = bmesh.from_edit_mesh(bpy.context.object.data)
bm.faces.ensure_lookup_table()
# Initialize list of vertices and list of parts to be selected
verts = []
to_select = []
# For VERT, EDGE, or FACE ...
# 1\. Grab list of global or local coordinates
# 2\. Test if the piece is entirely within the rectangular
# prism defined by lbound and ubound
# 3\. Select each piece that returned True and deselect
# each piece that returned False in Step 2
if select_mode == 'VERT':
if coords == 'GLOBAL':
[verts.append((world * v.co).to_tuple()) for v in bm.verts]
elif coords == 'LOCAL':
[verts.append(v.co.to_tuple()) for v in bm.verts]
[to_select.append(in_bbox(lbound, ubound, v)) for v in verts]
for vertObj, select in zip(bm.verts, to_select):
vertObj.select = select
if select_mode == 'EDGE':
if coords == 'GLOBAL':
[verts.append([(world * v.co).to_tuple()
for v in e.verts]) for e in bm.edges]
elif coords == 'LOCAL':
[verts.append([v.co.to_tuple() for v in e.verts])
for e in bm.edges]
[to_select.append(all(in_bbox(lbound, ubound, v)
for v in e)) for e in verts]
for edgeObj, select in zip(bm.edges, to_select):
edgeObj.select = select
if select_mode == 'FACE':
if coords == 'GLOBAL':
[verts.append([(world * v.co).to_tuple()
for v in f.verts]) for f in bm.faces]
elif coords == 'LOCAL':
[verts.append([v.co.to_tuple() for v in f.verts])
for f in bm.faces]
[to_select.append(all(in_bbox(lbound, ubound, v)
for v in f)) for f in verts]
for faceObj, select in zip(bm.faces, to_select):
faceObj.select = select
Listing 3-10.Function for Selecting Pieces
of Objects by Location
清单 3-11 给出了一个使用ut.act.select_by_loc()
选择球体的一部分并变换它们的例子。请记住,该函数的前两个参数是 3D 矩形棱柱的最低角和最高角。如果整个块(顶点、边、面)落在矩形棱柱内,它将被选中。
import ut
import importlib
importlib.reload(ut)
import bpy
# Will fail if scene is empty
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_uv_sphere_add(size=0.5, location=(0, 0, 0))
bpy.ops.transform.resize(value = (5, 5, 5))
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
# Selects upper right quadrant of sphere
ut.act.select_by_loc((0, 0, 0), (1, 1, 1), 'VERT', 'LOCAL')
# Selects nothing
ut.act.select_by_loc((0, 0, 0), (1, 1, 1), 'VERT', 'GLOBAL')
# Selects upper right quadrant of sphere
ut.act.select_by_loc((0, 0, 0), (5, 5, 5), 'VERT', 'LOCAL')
# Mess with it
bpy.ops.transform.translate(value = (1, 1,1))
bpy.ops.transform.resize(value = (2, 2, 2))
# Selects lower half of
sphere
ut.act.select_by_loc((-5, -5, -5), (5, 5, -0.5), 'EDGE', 'GLOBAL')
# Mess with it
bpy.ops.transform.translate(value = (0, 0, 3))
bpy.ops.transform.resize(value = (0.1, 0.1, 0.1))
bpy.ops.object.mode_set(mode='OBJECT')
Listing 3-11.Selecting and Transforming Pieces
of a Sphere
检查点和示例
到目前为止,我们已经对ut.py
做了很多补充。如需最新版本,包括我们迄今为止在书中添加的所有内容,请访问blender.chrisconlan.com/ut_ch03.py
。
鉴于这个版本的ut.py
,我们将尝试一些有趣的例子。随机形状增长算法见清单 3-12 。一个简单的算法随机地(草率地)选择物体所在的一块空间,然后沿着所选表面的垂直法线挤出所选部分。要沿表面的垂直法线挤出,我们只需运行ut.act.extrude((0, 0, 1))
,因为该功能默认使用表面的局部方向。
该算法让我们既能构建优雅的形状,也能构建古怪的形状。结果的类型主要取决于我们在脚本顶部附近的ut.create
调用中提供的形状。参见图 3-6 和 3-7 分别列出立方体和球体 3-12 的示例。
图 3-7。
Random sphere extrusion with 1000 iterations
图 3-6。
Random cube extrusion with 500 iterations
import ut
import importlib importlib.reload(ut)
import bpy
from random import randint
from math import floor
# Must start in object mode
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create a cube
bpy.ops.mesh.primitive_cube_add(radius=0.5, location=(0, 0, 0))
bpy.context.object.name = 'Cube-1'
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="DESELECT")
for i in range(0, 100):
# Grab the local coordinates
coords = ut.coords('Cube-1', 'LOCAL')
# Find the bounding box for the object
lower_bbox = [floor(min([v[i] for v in coords])) for i in [0, 1, 2]]
upper_bbox = [floor(max([v[i] for v in coords])) for i in [0, 1, 2]]
# Select a random face 2x2x1 units wide, snapped to integer coordinates
lower_sel = [randint(l, u) for l, u in zip(lower_bbox, upper_bbox)]
upper_sel = [l + 2 for l in lower_sel]
upper_sel[randint(0, 2)] -= 1
ut.act.select_by_loc(lower_sel, upper_sel, 'FACE', 'LOCAL')
# Extrude the surface along it aggregate vertical normal
bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate =
{"value": (0, 0, 1),
"constraint_axis": (True, True, True),
"constraint_orientation" :'NORMAL'})
Listing 3-12.
Random Shape Growth
虽然这些例子看起来微不足道,但它们展示了 Blender 中自动化编辑模式操作的强大功能。虽然清单 3-12 中的简单算法可以制作出迷人的形状,但是只要有正确的特定领域知识,其中的概念可以用来在 Blender 中创建完整的 CAD 系统。很好的例子包括:
- 商业建筑模型
- 数学曲面模型
- 原子和化学模型
所有这些都可以通过本章讨论的概念来实现。就目前情况而言,我们的工具包不是非常针对具体情况的。它有许多可以改进的地方,以适应不同学科和应用的建模需求。定制和改进我们的工具包的显著方法包括:
- 创建支持矩形棱柱以外的选择区域的
ut.act.select_by_loc()
函数。圆柱形、球形、二维和一维选择表面都有潜在的用途。 - 为它们创建额外的
ut.create
函数和特定案例的自动命名模式。 - 以与添加
ut.act.extrude
和ut.act.subdivide
相同的方式向ut.act
添加额外的编辑模式操作。有充分的机会来探索和进一步参数化这些功能。 - 增加
LOCAL
、NORMAL
、GIMBAL
轴操作到ut.sel
。到目前为止,我们一直使用默认的GLOBAL
。例如,平移、旋转和缩放都可以沿着这些轴执行。
结论
在接下来的章节中,我们将讨论在 Blender 中进行有效的插件开发所需的基本渲染概念。
四、建模和渲染主题
本章介绍并详述了 3D 建模和渲染中的特定主题。虽然非常笼统,但随着我们构建更高级的工具和插件,这些主题在第五章和文本的其余部分变得很重要。向读者介绍了 3D 艺术家、游戏开发者和渲染软件工程师通常知道的许多实用程序和陷阱。有了这些知识,读者将能够更好地满足这些脚本和插件开发专业人员的需求。
指定 3D 模型
3D 模型是复杂的数字资产,可以由许多不同的组件组成。我们通常认为网格是构成资产形状的最重要的结构,网格由面组成,面由按索引排列的顶点组成。网格可以包含法线向量或法线,可以用顶点或面来指定,具体取决于文件格式。当我们在摘要中提到这些术语时,我们通常是在讨论 3D 建模主题,而不是在 Blender 中具体定义它们。
我们从纯网格开始讨论 3D 模型,网格由顶点、索引、面和法线组成。从那里,我们讨论更先进的和具体的三维模型的特点,作为我们的网格讨论的延伸。
指定网格
为了本章的目的,我们认为一个基本的网格是由它的面和法向量定义的。请参见上述组件的以下定义:
- 顶点是指定 3D 空间中位置的实值三元组,通常表示为(x,y,z)。出于我们讨论的原因,在指定 3D 网格的文件中多次指定同一个点是很常见的。在 3D 建模中,z 轴或 y 轴最常用来表示垂直轴。在 Blender 中,z 轴是垂直轴。我们将在整个文本中使用这种格式。
- 索引是正整数值的三元组,使用一系列顶点指定面,通常表示为(I,j,k)。给定索引为 1,…,N 的 N 个顶点的列表,3D 空间中的面可以由 1,…,N 中的任意三个唯一整数的三元组来指定。这个概念非常符合逻辑地扩展自身,允许我们通过重用预先指定的顶点来定义网格。出于我们解释的原因,整数的顺序对于确定人脸可见的方向很重要。在实践中,索引重用元组值的概念通常扩展到其他元组,如法线和 uv。
- 面由引用三个顶点的整数三元组索引确定。根据我们的定义,我们很自然地得出这样一个事实,即 3D 空间中的三顶点人脸总共需要九个实值数据点。需要注意的是,3D 空间中的面仅在一个方向上可见。给定旋转相机和 3D 空间中的单个面部,用户将只能从单个方向观看面部。从另一个方向看,面将完全透明。这是许多 3D 渲染器的固有和预期行为,我们将学习控制这些行为。请注意,Blender 在默认情况下不会表现出这种单向行为,但是在导出为其他文件格式时,Blender 不会自动控制或纠正这种行为。
- 法向量是实值三元组,定义网格如何与场景中的灯光和相机交互。目前,我们只关心法线,因为它们直接分配给点,而不是 3D 艺术家可能已经熟悉的法线贴图。顾名思义,场景中的相机和照明与网格交互,假设法线向量垂直于它所照亮的面。这并不总是一个无关紧要的问题,正如我们将在立方体例子中看到的那样。法向量也影响面的可视方向和透明方向,如面的定义中所述。
指定纹理
3D 模型中纹理的目的是将 2D 图像映射到 3D 表面上,通常使用现有的 2D 艺术资产。我们使用的坐标惯例是(u,v)坐标系。在数学的其他领域,当讨论 3D 表面的 2D 投影时,我们通常使用(u,v)坐标系来清楚地表示我们在与(x,y,z)坐标系分离的空间中工作。
纹理坐标非常直观。如果我们想要在一个矩形表面上拉伸一个图像,我们指定 uv 坐标列表[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)]
来在我们正在看的表面上从左到右拉伸完整的图像。这是假设从我们的角度来看,模型中的坐标 1 到 4 代表表面的左下、右下、左上和右上坐标。见图 4-1 的一个普通纹理方案的例子。
图 4-1。
Vanilla texturing scheme on a cube
如果我们想在整个表面上拉伸、收缩或复制图像,我们只需通过适当的因子来调整 uv 坐标。例如,在立方体表面平铺图像三次,我们输入 uv 坐标[(0.0, 0.0), (3.0, 0.0), (0.0, 3.0), (3.0, 3.0)]
。参见图 4-2 的平铺纹理示例。
图 4-2。
Repeated texturing scheme on a cube
在第八章之前,我们不会通过 Blender 的 Python API 处理纹理,但是当我们讨论 3D 模型和文件格式时,理解纹理的 uv 坐标的概念是很重要的。
常见文件格式
我们首先列出常见的文件格式,并解释它们各自的优点和用途。我们结合本章开始时对 3D 对象的定义使用这些格式来进一步说明这些概念。
波前(。obj 和。mtl)
波前几何(.obj
)和材料(.mtl
)规范格式共同指定网格和纹理。它们是以这样的方式编写的,即.obj
文件可以独立地指定唯一的几何图形。.obj
文件非常小且易于理解,非常适合用作讨论 3D 对象形状的标准符号。
见清单 4-1 中 xy 平面上简单正方形的例子。obj 格式。我们将避免解释。mtl 文件,因为它与我们对渲染概念的讨论不太相关。
# Use hashes to leave comments in .obj files
# The 'o' tag is used to name objects
# all data following an 'o' tag is considered
# to have this name until another name is entered o MySimpleFace
# Vertices are entered with the 'v' tag as
# space-delimited (x, y, z) tuples
v -1.00 0.00 1.00
v 1.00 0.00 1.00
v -1.00 0.00 -1.00
v 1.00 0.00 -1.00
# Texture coordinate are entered with the 'vt' tag as
# space-delimited (u, v) tuples, between 0 and 1
vt 0.00 1.00
vt 1.00 1.00
vt 0.00 0.00
vt 1.00 0.00
# Normal vectors are entered with the 'vn' tag as
# space-delimited (x, y, z) tuples, can be normal vectors if desired vn 0.0000 1.0000 0.0000
# Indices are entered with the 'f' (for face) tag as
# space-delimited triplets of v, vt, and vn indices as
# f v_i/vt_i/vn_i v_j/vt_j/vn_j v_k/vt_k/vn_k
# Faces can have any number (three or more) coplanar points f 2/2/1 3/3/1 1/1/1
f 2/2/1 4/4/1 3/3/1
# Alternatively, the faces section for this face can be
# written as a single coplanar quadrilateral: f 1/1/1 2/2/1 4/4/1 3/3/1
# Alternatively, the texture coordinates can be
# excluded with double slashes f 1//1 2//1 4//1 3//1
Listing 4-1.Simple Square in the .obj Format
我们在清单 4-1 中看到了一个具有以下特征的简单面的.obj
文件格式的规范:
- 两个单位长,两个单位宽
- 以原点为中心,法向量沿 z 轴向上
- 一些纹理沿着正 x 轴和 y 轴定向
我们将在下面的例子中看到,.obj
格式与其他格式相比相当成熟和灵活。
立体平版印刷术
工程师和 CAD 软件通常使用 STL 文件格式。与.obj
格式相比,它显得冗长,但是附带了一个二进制规范来弥补它的低效。大多数 STL 导出器(包括 Blender)默认使用二进制规范,如果没有特殊软件的帮助,文件对人来说是难以辨认的。在这个文本中,我们只处理文件的文本格式。
参见清单 4-2 中我们的简单面,如 STL 格式中指定的清单 4-1 。STL 支持法向量和面,但不使用索引或支持纹理坐标。从清单 4-2 中可以看出,我们必须指定同一个法向量两次,总共六个顶点来指定 STL 中的一个四边形面。此外,STL 不支持超过三个共面点的规范。奇怪的是,大多数 3D 文件格式都允许将法向量分配给点,而 STL 只允许在面级别分配法向量。
这种结构相当简单明了。每个facet normal x y z
初始化一个面,然后每个outer loop-endloop
对保存该面的有序顶点。每个顶点被指定为循环中的vertex x y z
。
solid MyFace
facet normal 0.0 0.0 1.0
outer loop
vertex -1.0 -1.0 0.0
vertex 1.0 -1.0 0.0
vertex -1.0 1.0 0.0
endloop
endfacet
facet normal 0.0 0.0 1.0
outer loop
vertex 1.0 -1.0 0.0
vertex 1.0 1.0 0.0
vertex -1.0 1.0 0.0
endloop
endfacet
endsolid MyFace
Listing 4-2.Simple Face in the STL Format (Text Form)
PLY(多边形文件格式)
这种文件格式是由斯坦福大学开发的,用于 3D 扫描软件。它与 C 语言有密切的渊源,并且有许多直接使用 C 语言的开源工具。我们对 3D 网格格式的讨论应该开始感觉重复了。PLY 格式本质上是一个精简版的.obj
,增加了元数据,只支持顶点和面,不支持法线向量或纹理。
头中的一些元数据相当标准,包括ply
、format
和property
标签。我们不会深究property
标签。只知道他们引用 C 级数据类型是为了和现有的 C 库合作。element vertex
和element face
行分别指定文件中有多少行引用顶点和面。在我们的例子中,我们有element vertex 4
和element face 1
,因为我们正在指定一个面。值得注意的是,PLY 格式支持三个以上共面点的规范。
参见清单 4-3 中的面部示例。
ply
format ascii 1.0
comment specifies a simple faceelement vertex 4
property float32 xproperty float32 yproperty float32 zelement face 1
property list uint8 int32 vertex_indices end_header
-1 -1 0
1 -1 0
1 1 0
-1 1 0
4 1 0 3 2
Listing 4-3.Simple Face in the PLY Format
Blender(。混合)文件和交换格式
特别是根据前面的例子,Blender 的原生文件格式和内存中的数据结构非常复杂。Blender 支持对顶点、边和具有非共面顶点的面进行操作。与此同时,Blender 管理与纹理、声音、动画、装备、灯光等相关的复杂数据。这些.blend
文件是以二进制表示的,不适合人类阅读。谢天谢地,我们可以继续通过 Python API 安全地访问和操作 Blender 的内部数据。
.blend
文件与前述的.obj
、.stl
和.ply
文件在复杂性和完整性上的差异是有意的。虽然所有这些文件都以这样或那样的方式表示 3D 模型,但是.blend
文件并不是为在其他 3D 建模套件中导出和导入而设计的。上面讨论的文件格式被称为交换格式,这意味着它们有意地表示可以在建模软件和渲染器之间轻松移植的公共且定义良好的功能子集。
虽然开发人员在过去已经尝试在特定的 3D 建模套件(如 3DS Max、AutoCAD、Maya 和 Blender)之间创建完整的互操作性,但他们必然无法捕获任何一个套件支持的所有功能。因此,我们决定采用交换格式来保持沟通和期望的一致性。
基本对象的最小规格
讨论 3D 模型规范背后的一些理论很重要,这样我们可以评估各种 3D 文件格式的效率和能力。我们将引用上一节中讨论的文件格式来帮助说明。
立方体的定义
立方体是有六个面的三维物体,由等长的正方形组成。一个立方体包含 6 个面、12 条边和 8 个顶点。立方体的正方形面可以看作是两个直角三角形的合成,其边长等于正方形的边长。注意,3D 空间中的任何对象都可以由浮点和整数值来定义,其中浮点指定 3D 空间中的位置和方向,而整数指定相关的索引。3D 对象也需要法向量,法向量可以指定给顶点或面。
我们将使用这些信息来构建表格,详细说明不同 3D 规范模式的数据密度。
简单规范
为了简单地指定一个 3D 立方体,我们将独立地指定 6 * 2 = 12 个所需的三角形面中的每一个,并为每个点指定一个独立的法向量。这将产生 12 * 3 = 36 个顶点和 12 * 3 = 36 个法向量。我们可以用.obj
格式来写,如清单 4-4 所示。图 4-3 显示了该模型数据结构的可视化。
图 4-3。
Data structure of naively specified cube
这种模式的天真是由以下因素定义的:
- 不必要地重复顶点坐标
- 法向量方向的不必要重复
- 不必要的使用顶点法线代替面法线
o NaiveCube
# (36 * 3) + (36 * 3) = 216 floats
# (12 * 3) + (12 * 3) = 72 integers
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 1.000000 1.000000 -0.999999
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v 0.999999 1.000000 1.000001
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v -1.000000 -1.000000 1.000000
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 -0.999999
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 -1.000000
v -1.000000 1.000000 -1.000000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 -0.0000 0.0000
vn 0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 -0.0000 0.0000
vn 0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 -0.0000 0.0000
vn 0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 -0.0000 0.0000
vn 0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 -0.0000 0.0000
vn 0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 -0.0000 0.0000
vn 0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
f 9//1 17//13 13//25
f 24//2 20//14 16//26
f 15//3 12//15 10//27
f 6//4 18//16 2//28
f 3//5 23//17 14//29
f 1//6 8//18 5//30
f 29//7 11//19 32//31
f 36//8 22//20 34//32
f 31//9 19//21 30//33
f 27//10 21//22 33//34
f 26//11 7//23 35//35
f 25//12 4//24 28//36
Listing 4-4.Naively Defined Cube
换句话说,简单的 3D 规范不会通过将每个面视为完全独立的三角形来通过索引重用顶点或法线。此外,在立方体等简单情况下,使用顶点法线而不是面法线会增加浪费。这种模式将大大受益于:
- 移除重复的顶点
- 将三角形面指定为正方形面
- 移除重复法线和/或使用面法线
- 正确利用索引组织顶点和法线
值得注意的是,这种格式与禁止 STL 使用面法线的.stl
格式具有相同的复杂程度。
接下来我们将展示非重复顶点和法线如何在不增加复杂性的情况下缩小模型尺寸。
使用索引共享顶点和法线
清单 4-5 显示了一个共享顶点和法线的.obj
文件。当文件能够正确使用索引并且不重复浮点数据时,我们总共只需要 42 个浮点。在下一个例子中,我们将利用共面曲面来减少整数的总数。从视觉上看,该数据与图 4-3 中的数据相同,我们只是减少了浮动数据中的重复。
o SharingCube
# (8 * 3) + (6 * 3) = 42 floats
# (12 * 3) + (12 * 3) = 72 integers
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 -0.0000 0.0000
vn 0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
f 1//1 3//1 4//1
f 8//2 6//2 5//2
f 5//3 2//3 1//3
f 6//4 3//4 2//4
f 3//5 8//5 4//5
f 1//6 8//6 5//6
f 1//1 2//1 3//1
f 8//2 7//2 6//2
f 5//3 6//3 2//3
f 6//4 7//4 3//4
f 3//5 7//5 8//5
f 1//6 4//6 8//6
Listing 4-5.Cube with Shared Vertices and Normals
使用共面顶点减少面数
清单 4-6 显示了一个.obj
文件,其中立方体的每个面都被整体指定。因为我们知道立方体的面都是共面点的集合,所以我们可以将它们指定为一个面。虽然渲染器仍然将立方体解释为三角形面的集合,但是.obj
文件格式允许我们指定具有共面顶点的 N 维表面。图 4-4 显示了这种数据结构的可视化表示。
图 4-4。
Face-planar, vertex-sharing, normal-sharing cube
o CoplanarFaceCube
# (8 * 3) + (6 * 3) = 42 floats
# (6 * 4) + (6 * 4) = 48 integers
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
vn -1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
f 1//1 2//1 4//1 3//1
f 3//2 4//2 8//2 7//2
f 7//3 8//3 6//3 5//3
f 5//4 6//4 2//4 1//4
f 3//5 7//5 5//5 1//5
f 8//6 4//6 2//6 6//6
Listing 4-6.Cube with Coplanar Surfaces
as Single Faces
这里没有太多的重复。最后一个重复特征是每个面的每个点的法向量索引的规格。接下来我们给出一个使用面顶点的理论上的.obj
文件。
使用面顶点简化索引
清单 4-7 显示了一个理论上的.obj
文件,其中立方体的每个面都被赋予了相同的法线。因为立方体有明确定义的面法线,所以我们很容易在数据结构中指定它们。通常在.obj
文件中,我们会重复指定顶点法线的索引。然后,渲染过程将计算顶点法线的合成,以确定如何对相关的面进行着色。在这个理论上的.obj
文件中,我们将在面级别而不是点级别指定顶点索引。该文件被称为“理论上的”,因为.obj
文件实际上并不支持面法线,尽管其他常见的文件格式支持。为了保持一致,我们将继续使用本例中的.obj
格式,但是请注意,这个文件没有导入。参见图 4-5 了解该数据结构的可视化表示。
图 4-5。
Cube with face normals
下一次迭代将通过将立方体二进制化到渲染器本身来降低复杂性。这是一个特例,仅适用于非常普通和简单的形状。开发人员很少有能力在渲染器本身中定制这种功能,但这是值得注意的。
o FaceNormalsCube
# Theoretical .obj format, not valid
# (8 * 3) + (6 * 3) = 42 floats
# (6 * 4) + (6 * 1) = 30 integers
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
vn -1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
# Face and normals defined as:
# f (v_1, v_2, v_3, v_4)//n_1
f (1 2 4 3)//1
f (3 4 8 7)//2
f (7 8 6 5)//3
f (5 6 2 1)//4
f (3 7 5 1)//5
f (8 4 2 6)//6
Listing 4-7.Cube with Face Normals
将立方体表示为原语
图元泛指预构建到 3D 软件包中的非常常见的对象。最值得注意的是,渲染器中的原语是通用对象的二进制版本,它总是优于等效的文本文件规范(.obj
、.stl
等)。)在加载时间。如果可能的话,查看渲染器的文档,寻找机会使用渲染器的默认图元添加简单的对象,通常是立方体、球体、圆柱体、圆锥体和圆环。这并不是说对象的内存规格在空间上比其他规格更有效,而只是说它们具有已经被二进制化的优势。
摘要
我们依次讨论了四种更有效的指定立方体的方法,最后选择使用原语。请参见下表了解结果摘要。我们已经通过命令行工具确定了.obj
文件的大小。我们估计了对象在内存中的大小,因为在 32 位系统中,C++中的浮点数和整数是四个字节。在每一步中,内存大小的百分比变化都足够大,足以证明进行这些效率调整的合理性。
三元组共享的内存百分比变化是一个令人信服的理由,只要有可能,总是支持.obj
和.ply
而不是.stl
。熟悉本节讨论的信息对于 Blender Python API 开发人员来说至关重要。虽然 Blender 非常强大,但它给了我们既浪费又高效的机会。
过程生成中的常见错误
我们使用本章中建立的语言来说明过程化生成的模型的一些常见问题,以及采取什么步骤来调试它们。
同心法线
当生成模型并导出到各种交换和渲染格式时,法向量很容易被忽略或错误分配。Blender 在 3D 视口中为我们处理大部分正常的矢量管理过程,所以这些问题很少在导出前被发现。我们遇到的一个非常常见的错误是无法解释的不稳定照明。这个问题通常归结为正常的管理,可以通过 Blender 本身的一些函数调用或按钮点击来解决。
参见清单 4-8 中一个立方体的.obj
文件的例子,它被错误地赋予了同心法线。参见清单 4-9 中用平面法线正确导出立方体的例子。这些立方体每个都在 WebGL 中渲染,以显示当导出到其他渲染器时法线如何影响照明。同心和平面立方体模型效果图见图 4-6 和 4-7 。
图 4-7。
Planar normals (flat shading) in WebGL
图 4-6。
Concentric normals (smooth shading) in WebGL
同心立方体被照亮和着色,就好像它是一个球体,而平面立方体被逻辑地照亮和着色,将顶面视为一种桌面。浏览清单 4-8 ,我们看到立方体中的每个顶点都与一个法向量匹配,该法向量等于按 1/√3 ≈ 0.5773 缩放的顶点。在某些导出器中,这是一种危险的行为,如果没有找到明确的法线信息,它将默认从缩放的顶点创建单位向量。这可以防止导出器失败,但会导致照明不佳且经常无法识别的对象。
这个问题对于通常处理大平面的硬表面建模者来说是常见的。对于创建高多边形模型的有机建模者来说,这个问题更容易无法诊断。
o ConcentricCube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vn 0.5773503 -0.5773503 -0.5773503
vn 0.5773503 -0.5773503 0.5773503
vn -0.5773503 -0.5773503 0.5773503
vn -0.5773503 -0.5773503 -0.5773503
vn 0.5773503 0.5773503 -0.5773497
vn 0.5773497 0.5773503 0.5773508
vn -0.5773503 0.5773503 0.5773503
vn -0.5773503 0.5773503 -0.5773503
f 1//1 3//3 4//4
f 8//8 6//6 5//5
f 5//5 2//2 1//1
f 6//6 3//3 2//2
f 3//3 8//8 4//4
f 1//1 8//8 5//5
f 1//1 2//2 3//3
f 8//8 7//7 6//6
f 5//5 6//6 2//2
f 6//6 7//7 3//3
f 3//3 7//7 8//8
f 1//1 4//4 8//8
Listing 4-8.Cube with Concentric Normals
o PlanarCube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn -0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
f 1//1 2//1 3//1 4//1
f 5//2 8//2 7//2 6//2
f 1//3 5//3 6//3 2//3
f 2//4 6//4 7//4 3//4
f 3//5 7//5 8//5 4//5
f 5//6 1//6 4//6 8//6
Listing 4-9.Cube with Planar Normals
Note
Blender 的内置导出器(包括.obj
和.stl
)在正常情况下很少会表现出这种行为。这种行为对于附加出口商和其他第三方出口商更为常见。
为了举例,在本文中定义了同心和平面阴影。虽然绝不等同,但是当分别误用平滑阴影和平坦阴影时,会出现类似的问题。平滑着色是指使用相邻面法线的组合为每个顶点创建一个法线,而平滑着色是指使用每个单独的面法线为每个顶点创建多个法线。在一个单位立方体的情况下,同心法线和平滑着色看起来是等价的,而面法线和平面着色看起来是等价的。
这个问题可以通过几种方式解决,具体取决于具体的出口商。在许多情况下,目标文件格式不支持面级法线或面法线,所以我们必须强制 Blender 使用顶点级法线或顶点法线。在这种情况下,我们让 Blender 为每个顶点创建多个实例,以便它可以为每个顶点分配单独的法线。在我们的立方体示例中,立方体的每个顶点都连接到三个独立的面,因此需要三个独立的顶点法线。
我们可以使用“边分割”修改器来实现这一点。这可以在属性➤修改器➤添加修改器➤边分割中找到。根据您的喜好调整分割阈值,然后选择“应用”。查看清单 4-10 获取访问这个修改器的 Blender Python 方法。这可以很容易地包装在一个函数中,并且可以很好地放入前面章节中建立的ut.sel
函数类中。
# Add modifier to selected objects
bpy.ops.object.modifier_add(type='EDGE_SPLIT')
# Set split threshold in radians
bpy.context.object.modifiers["EdgeSplit"].split_angle = (3.1415 / 180) * 5
# Apply modifier
bpy.ops.object.modifier_apply(apply_as='DATA', modifier='EdgeSplit')
Listing 4-10.Cube with Planar Normals
结果表明是非常有效的。图 4-8 和 4-9 显示了在 Blender 中查看之前和之后的法向量。
图 4-9。
Normal vectors after edge split (flat shaded)
图 4-8。
Normal vectors before edge split (smooth shaded)
翻转法线
另一个常见的问题是无意中翻转法线。由于 Blender 的 3D 视口的某些行为,这个问题可能会影响 Blender Python 程序员。如前所述,翻转的法线可以使平面看起来透明。这通常很难在 Blender 中进行诊断,因为 Blender 在 3D 视口中将所有平面都视为双面。这是不直观的,因为为了性能和一致性,普通渲染器将平面视为单侧。
在图 4-8 和 4-9 中,我们画了法向量来表示它们所指的方向。在这两个图中,法线清楚地指向对象的外部,因此在导出时没有遇到翻转法线的危险。图 4-10 和 4-11 显示了在 WebGL 中渲染的立方体的两个透视图,其中一个面上有翻转的法线。正如我们在这些图中看到的,具有翻转法线的面是透明的,我们期望在它后面看到的面也是透明的,因为我们是从后面看到它们的。从数学上讲,这可以通过将每个翻转的法向量缩放 1 来解决。在 Blender 中,这可以通过进入编辑模式并导航到工具架➤着色/ UVs ➤着色➤法线➤翻转方向相当容易地执行。该按钮将根据选定的部分翻转所有选定的、顶点、边或面的法线。
图 4-11。
Cube with flipped normals on single face (perspective #2)
图 4-10。
Cube with flipped normals on single face (perspective #1)
在 Blender 的 Python API 中,我们可以通过在编辑模式下调用bpy.ops.mesh.flip_normals()
来执行相同的功能,同时选择对象的某些部分。复杂的程序生成通常会产生方向不佳的法线,可以使用此功能在生成后进行校正。
工具架➤着色/ UVs ➤着色➤法线➤重新计算命令调用bpy.ops.mesh.normals_make_consistent()
,将告诉 Blender 尽最大能力重新计算定义明确的对象的法线。这并不是对每个对象都适用,但仍然很有用。
z 型格斗
Z-fighting 是一个常见的渲染问题,它会在不抛出错误或使渲染器崩溃的情况下产生不正常的对象。大多数动画师和游戏玩家都很熟悉这个问题,不管他们是否听过这个术语。请参见图 4-12 查看渲染视图中 Blender 中四个立方体之间的 Z 战斗示例。
图 4-12。
Z-fighting of cubes with coplanar faces
为了理解为什么会发生 Z-fighting,我们必须理解深度缓冲区在渲染器中是如何工作的。几乎在每种情况下,渲染对象所涉及的计算都发生在具有非常标准化的图形 API(例如,OpenGL 和 DirectX)的图形处理单元(GPU)上。这些渲染 API 中的标准协议是使用相机相对于网格的位置来确定哪些对象对用户是可见的,哪些是不可见的。该信息存储在深度缓冲器中。在屏幕上显示 2D 图像之前,深度缓冲区会告诉渲染器哪个网格像素最靠近相机,因此对用户可见。
鉴于这一信息,为什么深度缓冲区不倾向于一个网格而不是另一个网格来防止虚假的 Z-fighting 效果?深度缓冲区存储高精度浮点值,渲染器不会进行调整来评估浮点数的相等性。驱动图形 API 的低级语言通过进行简单的浮点数比较来保持效率。与 Python 中的0.1 * 0.1 > 0.01
返回True
的原因相同,浮点数比较在呈现器中的行为不一致。与浮点运算相关的问题在计算机科学中得到了很好的研究,浮点等式是其最重要的挑战之一。
给定 Blender 中的工具及其 Python API,如何解决这个问题呢?根据具体情况,有许多解决方案。
- 将每个对象平移一个很小且不明显的量(大约在
0.000001
Blender 单元周围),使表面不再共面。如果平移没有效果,可以尝试平移稍微大一点的距离。 - 在编辑模式下删除内部面。
- 重新调整算法以生成不重叠的表面。
- 在编辑模式下使用“融合”和“限制融合”工具。
最终,有许多方法可以处理 Z-fighting,所有这些方法都可以确保模型中不再存在共面曲面。我们避免详述所有可能的方法。
结论
重要的是要记住,Blender 已经从这里讨论的许多低级 3D 建模概念中抽象出来。这对我们很有帮助,因为我们不必在大多数时候担心数据表示、着色语义和 Z-fighting。尽管如此,我们还是引入了这些概念,因为在调试时,意识到这些问题及其驱动因素可以避免很多麻烦。
五、插件开发简介
本章使用 Blender 的 Python API 构建基本的附加组件。插件开发的最大障碍之一是从一个开发环境过渡到一个包装整齐且独立于操作系统的插件,所以我们在本章花了相当多的时间讨论各种开发实践。在本章结束时,读者应该能够在开发和部署环境中注册简单的附加组件。接下来的章节将在这些知识的基础上,将更多的高级特性整合到附加组件中。
一个简单的附加模板
对于这一节,进入 Blender 的脚本视图,并进入文本编辑器➤新建创建一个新的脚本。给它起个名字,比如simpleaddon.py
。参见清单 5-1 中的简单模板,我们可以从这里开始构建我们的附加组件。运行这个脚本将在工具面板中创建一个名为“Simple Addon”的新标签,它有一个简单的文本输入框和一个按钮。该按钮将向控制台打印一条消息,验证插件是否工作,然后鹦鹉学舌般地重复文本输入字段中的字符串。附件 GUI 的外观和位置见图 5-1 。
图 5-1。
Simple add-on template
bl_info = {
"name": "Simple Add-on Template",
"author": "Chris Conlan",
"location": "View3D > Tools > Simple Addon",
"version": (1, 0, 0),
"blender": (2, 7, 8),
"description": "Starting point for new add-ons.",
"wiki_url": "http://example.com",
"category": "Development"
}
# Custom modules are imported here
# See end of chapter example for suggested protocol
import bpy
# Panels, buttons, operators, menus, and
# functions are all declared in this area
# A simple Operator class
class SimpleOperator(bpy.types.Operator):
bl_idname = "object.simple_operator"
bl_label = "Print an Encouraging Message"
def execute(self, context):
print("\n\n####################################################")
print("# Add-on and Simple Operator executed successfully!")
print("# " + context.scene.encouraging_message)
print("####################################################")
return {'FINISHED'}
@classmethod
def register(cls):
print("Registered class: %s " % cls.bl_label)
# Register properties related to the class here
bpy.types.Scene.encouraging_message = bpy.props.StringProperty(
name="",
description="Message to print to user",
default="Have a nice day!")
@classmethod
def unregister(cls):
print("Unregistered class: %s " % cls.bl_label)
# Delete parameters related to the class here
del bpy.types.Scene.encouraging_message
# A simple button and input field in the Tools panel
class SimplePanel(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_category = "Simple Addon"
bl_label = "Call Simple Operator"
bl_context = "objectmode"
def draw(self, context):
self.layout.operator("object.simple_operator",
text="Print Encouraging Message")
self.layout.prop(context.scene, 'encouraging_message')
@classmethod
def register(cls):
print("Registered class: %s " % cls.bl_label)
# Register properties related to the class here.
@classmethod
def unregister(cls):
print("Unregistered class: %s " % cls.bl_label)
# Delete parameters related to the class here
def register():
# Implicitly register objects inheriting bpy.types in current file and scope
#bpy.utils.register_module(__name__)
# Or explicitly register objects
bpy.utils.register_class(SimpleOperator)
bpy.utils.register_class(SimplePanel)
print("%s registration complete\n" % bl_info.get('name'))
def unregister():
# Always unregister in reverse order to prevent error due to
# interdependencies
# Explicitly unregister objects
# bpy.utils.unregister_class(SimpleOperator)
# bpy.utils.unregister_class(SimplePanel)
# Or unregister objects inheriting bpy.types in current file and scope
bpy.utils.unregister_module(__name__)
print("%s unregister complete\n" % bl_info.get('name'))
# Only called during development with 'Text Editor -> Run Script'
# When distributed as plugin, Blender will directly
# and call register() and unregister()
if __name__ == "__main__":
try:
unregister()
except Exception as e:
# Catch failure to unregister explicitly
print(e)
pass
register()
Listing 5-1.Simple Add-On Template
当我们运行这个脚本时,我们应该得到关于我们在清单 5-1 中声明的类的注册和注销的控制台输出。通过更改消息并选择打印鼓励消息,我们应该会在控制台中看到如下内容:
Unregistered class: Print an Encouraging Message Unregistered class: Call Simple Operator
Simple Add-on Template unregister complete
Registered class: Print an Encouraging Message Registered class: Call Simple Operator
Simple Add-on Template registration complete
####################################################
# Add-on and Simple Operator executed successfully!
# Have a nice day!
####################################################
####################################################
# Add-on and Simple Operator executed successfully!
# I changed the message!
####################################################
尽管有许多细节需要解释,Blender 插件还是相当优雅和易读的。虽然每一行代码都有一个目的,但脚本通过重复受益于一致性。图 5-1 中展示的模板相当简单,但是我们也包括了一些可选的质量控制。我们先讨论每个组件,然后再讨论更高级的附加组件。
Blender 附件的组件
Blender 插件依赖于许多不同的和特别命名的变量和类函数来正常运行。我们在这里按类别详细介绍它们。
商业信息词典
出现在 Blender 附加组件中的第一个东西应该是bl_info
字典。这个字典是从源文件的前 1024 个字节中解析出来的,所以bl_info
必须出现在文件的顶部。我们将使用字典这个词来指代dict
类的 Python 对象。
Blender 的内部引擎使用这个字典中的数据来填充与附加组件本身相关的各种元数据。如果我们导航到标题菜单➤文件➤用户偏好➤附加组件,我们可以看到各种官方和社区附加组件已经在 Blender。点击任何附加组件上的插入符号,显示如何使用bl_info
信息来填充这个 GUI,如图 5-2 所示。
图 5-2。
How Blender uses bl_info
值得注意的是,bl_info
字典对附加组件没有任何功能影响,而是决定了最终用户如何在这个窗口中找到并激活它。请参见此处的详细描述:
-
名称—插件在用户首选项的附加项选项卡中显示的名称(例如,
Math
Vis(控制台),运动捕捉工具)。它被写成单个字符串。 -
作者——出现在用户首选项中的一个或多个作者的姓名(例如,Campbell Barton、Fabian Fricke)。它可以是一个带逗号的字符串或一组字符串。
-
位置—附加组件 GUI 的主要位置。对于工具、属性和工具架面板中的附加组件,常用语法为窗口➤面板➤选项卡➤部分。如有疑问,请遵循其他附加组件建立的约定。
-
版本—以元组形式表示的附加组件的版本号。
-
blender——根据 Blender Wiki,这是运行插件所需的最低 Blender 版本号。当较低版本可以支持附加组件时,社区附加组件经常错误地将
(2, 7, 8)
列为版本。在许多情况下,这个数字指的是开发者选择支持的最低版本。 -
描述—显示在“用户首选项”窗口中的简短描述,指定为单个字符串。
-
wiki _ url 指向附加模块手册或指南的 url,指定为单个字符串。
-
category —A string specifying one the categories listed in Table 5-1.
表 5-1。
The bl-info Category Options
| 三维视图 | 作文 | 照明设备 | 目标 | 装备 | 文字编辑器 | | --- | --- | --- | --- | --- | --- | | 添加网格 | 发展 | 材料 | 颜料 | 事件 | 紫外线 | | 添加曲线 | 游戏引擎 | 网状物 | 物理学 | 序列发生器 | 用户界面 | | 动画 | 进出口 | 结节 | 提出 | 系统 | |
还有一些不太常见的选项。
- 支持—
OFFICIAL
、COMMUNITY
或TESTING
。其中官方指的是官方支持的 Blender 附加组件,社区指的是社区支持的附加组件,测试指的是应该有意从 Blender 版本中排除的未完成的或新的附加组件。 - tracker _ url—指向错误跟踪器的 URL(例如,GitHub 问题或类似问题)。
- 警告—指定将出现在用户首选项窗口中的某些警告的字符串。
运算符和类继承(bpy.types.Operator)
从最简单的意义上说,插件允许我们通过点击标准 Blender GUI 中的按钮来调用 Blender Python 函数。Blender GUI 调用的函数必须首先注册为类bpy.types.Operator
的操作符。以SimpleOperator
为例。当我们注册这个类时,对SimpleOperator.execute()
的调用被映射到bpy.ops
中的一个函数对象。它映射到的函数是由类头部的bl_idname
值决定的bpy.ops
。因此,在您运行清单 5-1 中的脚本之后,您可以通过从交互控制台、从附加组件本身或者从不相关的 Python 脚本中调用bpy.ops.object.simple_operator()
来打印一条鼓励性的消息。
下面是在 Blender 中声明一个操作符的步骤。请参考清单 5-1 中的SimpleOperator
类定义。
-
声明一个继承
bpy.types.Operator
的类。这将在我们的代码中显示为:class MyNewOperator (bpy.types.Operator):
-
将
bl_idname
声明为一个字符串,带有您选择的类和函数名,用句点分隔(例如,object.simple_operator
或simple.message
)。类名和函数名只能包含小写字符和下划线。执行功能稍后将在bpy.ops.my_bl_idname
可用。 -
(可选)声明一个
bl_label
作为描述该类函数的任何字符串。这将出现在 Blender 自动生成的函数文档和元数据中。 -
声明一个
execute
函数。这个函数将作为一个普通的类函数,并且总是接受对bpy.context
的引用作为参数。根据bpy.types.Operator
类的设计,execute
函数总是被定义为:def execute(self, context):
在一个操作符类中,如果成功调用了
execute()
,那么最好的做法是返回{"FINISHED"}
。 -
(可选)声明注册和注销类的类方法。
register
和unregister
函数总是需要@classmethod
装饰器,并将cls
作为参数。每当 Blender 试图注册或注销 operator 类时,都会运行这些函数。在开发过程中包含一个关于类注册和取消注册的打印声明是很有帮助的,就像我们在清单 5-1 中所做的那样,以检查 Blender 没有错误地重新注册现有的类。同样需要注意的是,我们可以在这些函数中声明和删除场景属性。我们将在后面的章节中讨论这一点。
为了确保 Blender 可以使用我们的 Python 代码,有一些限制和准则需要遵循。最终,这些指导方针改变了我们编码的方式和我们思考构建 Python 代码库的方式。这就是我们对 Blender Python API 的理解,它开始感觉像一个真正的应用编程接口(API),而不仅仅是有用函数的集合。
面板和类继承(bpy.types.Panel)
bpy.types.Panel
类是在附加组件中继承的下一个最常见的类。面板已经构成了 Blender 的大部分工具、工具箱和属性窗口。其中一个窗口的每个可折叠部分都是一个不同的面板。例如,如果我们导航到 3D 视口➤工具➤工具,我们会默认看到三个面板:变换,编辑和历史。在 Blender Python 插件中,这些将由三个不同的bpy.types.Panel
类来表示。
以下是注册面板的要求。参考清单 5-1 中的SimplePanel
类。
-
声明一个继承
bpy.types.Panel
的类。这将显示为class MyNewPanel(bpy.types.Panel):
。 -
Declare
bl_space_type
,bl_region_type
,bl_category
, andbl_label
. Readers may have noticed the ordering of these is intentional (though not necessary). These four variables, in the order written and in Listing 5-1, specify the path that the user takes to reach the panel. In Listing 5-1, this reads VIEW_3D ➤ TOOLS ➤ Simple Addon ➤ Call Simple Operator, which looks very familiar to the way we have located GUI elements thus far in the text. Correct case and spelling matter in these variables. While the category and label can be arbitrary values, the space and region must reference real areas of the Blender GUI. See Tables 5-2 and 5-3 for the list of possible arguments tobl_space_type
andbl_region_type
.表 5-3。
bl-region-type Options
| `WINDOW` | `HEADER` | `CHANNELS` | `TEMPORARY UI` | `TOOLS` | `TOOL_PROPS` | `PREVIEW` |表 5-2。
bl-space-type Options
| `EMPTY` | `NLA_EDITOR` | `NODE_EDITOR` | `INFO` | | `VIEW_3D` | `IMAGE_EDITOR` | `LOGIC_EDITOR` | `FILE_BROWSER` | | `TIMELINE` | `SEQUENCE_EDITOR` | `PROPERTIES` | `CONSOLE` | | `GRAPH_EDITOR` | `CLIP_EDITOR` | `OUTLINER` | | | `DOPESHEET_EDITOR` | `TEXT_EDITOR` | `USER_PREFERENCES` | |Most combinations of
bl_space_type
andbl_region_type
do not work together, but logical combinations will generally work. There is presently no complete documentation on which space types and region types cooperate. Also, not all space types and region types require a declaration ofbl_category
orbl_label
. Again, using them where logical typically gives good results. -
(可选)声明
bl_context
。和前面的例子一样,我们可以设置bl_context
等于objectmode
,让面板只出现在对象模式下。在撰写本文时,我们还没有这个变量的有效选项的具体列表。API 文档当前有一个 TODO 标记要求更多解释。我们将在后面的章节中介绍poll()
方法,这是实现这种行为的一种更加灵活的方式。 -
声明
draw
方法。这个函数将上下文作为一个参数,并且总是被声明为def draw(self, context):
。在这个函数定义中,需要注意的是,context
指的是bpy.context
对象,但不应该作为bpy.context
传递。这个函数体中的重要变量是bpy.context.scene
和self.layout
。layout.prop()
函数可以引用场景属性、对象属性和一些其他的 Blender 内部属性。它将根据场景属性本身自动创建适当的输入字段。清单 5-1 中的encouraging_message
场景属性被声明为字符串属性,因此将其作为参数提供给layout.prop()
会产生一个文本输入字段。layout.operator()
函数获取一个操作符的bl_idname
并创建一个标签由text = argument
指定的按钮。我们不会在这里详细讨论布局对象,因为对于高级 GUI 来说它会变得非常复杂。我们将在本章后面详细讨论布局对象。 -
(可选)用装饰器
@classmethod
声明register()
和unregister()
函数,就像我们讨论bpy.types.Operator
类一样。
注册()和取消注册()
在清单 5-1 的末尾附近是两个函数register()
和unregister()
,它们是附加组件中必需的。这两个函数负责调用bpy.utils.register_class()
、bpy.utils.unregister_class()
、bpy.utils.register_module()
和bpy.utils.unregister_module()
。任何继承了bpy.type
类的类都需要注册,以便 Blender 在插件中使用。当用户在用户首选项中关闭附加组件时,Blender 使用unregister()
功能。
我们有两种注册和注销类的选择。有些更适合开发,有些更适合部署。
- 显式注册和注销每个类。在这种情况下,我们希望按照逻辑顺序注册类。依赖于其他类的类应该在它们的依赖者之后注册。我们在使用
bpy.utils.register_class()
的register()
函数中这样做,将类名作为参数传递。应使用unregister()
功能中的bpy.utils.unregister_class()
以相反的顺序取消注册类别。 - 根据类在模块中的成员身份隐式注册和注销类。我们用
bpy.utils.register_module()
和bpy.utils.unregister_module()
函数来实现。我们经常看到bpy.utils.register_module(__name__)
在已发布的附加组件的register()
函数中被调用,但是在开发过程中可能会很混乱,我们稍后会解释。
回头看看清单 5-1 ,我们看到我们已经显式地注册了我们的类,但是隐式地取消了注册。在作者看来,这种设置非常适合单文件插件的实时编辑。bpy.utils.unregister_module(__name__)
的作用是清除在脚本之前运行中注册的类的附加环境。在使用 Blender 的文本编辑器进行编辑的过程中,bpy.utils.register_module(__name__)
经常会注册以前运行脚本时类的失效或未使用的副本。
因此,现场编辑附加组件的全新方法似乎是显式注册和隐式注销。隐式取消注册将从以前的运行中挑选出分散的类实例,然后显式注册只实例化当前运行中新创建的类。这违背了大多数文档的建议,它们通常建议使用清单 5-2 中的一种样式来注册和注销。我们在清单 5-1 中的方法是安全的、冗长的,并且可以很容易地修改以符合清单 5-2 中普遍接受的实践。
# Option 1:
# Using implicit registration
def register():
bpy.utils.register_module(__name__)
def unregister():
bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()
# Option 2:
# Using explicit registration
def register():
bpy.utils.register_class(SimpleOperator)
bpy.utils.register_class(SimplePanel)
def unregister():
bpy.utils.unregister_class(SimpleOperator)
bpy.utils.unregister_class(SimplePanel)
if __name__ == "__main__":
register()
# Option 3 (Recommended)
# Explicit registration and implicit unregistration
# With safe + verbose single-script run
def register():
bpy.utils.register_class(SimpleOperator)
bpy.utils.register_class(SimplePanel)
def unregister():
bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
try:
unregister()
except Exception as e:
print(e)
pass
register()
Listing 5-2.Registration Protocol
场景属性和 bpy.props
添加到Scene
和Object
类型的属性将被保存到.blend
文件中。为了让用户通过 Blender GUI 修改变量,他们必须注册为bpy.props.*
对象。bpy.props
类有大多数数据类型的选项,包括浮点数、整数、字符串和布尔值。他们可以注册到bpy.types.*
类,包括Scene
和Object
。在本节中,我们讨论如何将简单的场景属性注册到bpy.types.Scene.*
变量中。这些是可以通过bpy.context.scene.*
访问的任意命名的变量。虽然名称是任意的,但它仅限于小写字符和下划线。
我们可以在两个地方注册场景变量:
- 在脚本底部的
register()
函数中。 - 在继承了
bpy.types.*
类的任何类的register()
class 方法中(面板、操作符、菜单等)。).
最常见的情况是,场景变量直接绑定到一个类。为了清晰和组织,我们希望在该类的register()
classmethod 中声明这些变量。其他不适合类定义的变量可以在脚本底部的register()
函数中声明。在本文中,我们鼓励场景属性在register()
classmethod 中声明,如果与一个特定的类紧密相关的话,但是这在现有的社区附加组件中并不常见。
场景变量将是bpy.types.*
变量的实例。这些包括 Blender 类型StringProperty
、FloatProperty
、IntProperty
和BoolProperty
。任何时候面板通过调用self.layout.prop
在 GUI 中包含一个变量,该变量将根据其类型进行逻辑格式化。整数和浮点数出现在滑动条中,字符串显示为文本输入字段,布尔值显示为复选框,等等。
在清单 5-3 中,我们用额外的场景变量重新声明了清单 5-1 中的SimpleOperator
和SimplePanel
。读者将使用清单 5-1 作为模板重写这些类。参见图 5-3 获得最终图形用户界面。
图 5-3。
Exploring scene properties
# Simple Operator with Extra Properties
class SimpleOperator(bpy.types.Operator):
bl_idname = "object.simple_operator"
bl_label = "Print an Encouraging Message"
def execute(self, context):
print("\n\n####################################################")
print("# Add-on and Simple Operator executed successfully!")
print("# Encouraging Message:", context.scene.encouraging_message)
print("# My Int:", context.scene.my_int_prop)
print("# My Float:", context.scene.my_float_prop)
print("# My Bool:", context.scene.my_bool_prop)
print("# My Int Vector:", *context.scene.my_int_vector_prop)
print("# My Float Vector:", *context.scene.my_float_vector_prop)
print("# My Bool Vector:", *context.scene.my_bool_vector_prop)
print("####################################################")
return {'FINISHED'}
@classmethod
def register(cls):
print("Registered class: %s " % cls.bl_label)
bpy.types.Scene.encouraging_message = bpy.props.StringProperty(
name="",
description="Message to print to user",
default="Have a nice day!")
bpy.types.Scene.my_int_prop = bpy.props.IntProperty(
name="My Int",
description="Sample integer property to print to user",
default=123,
min=100,
max=200)
bpy.types.Scene.my_float_prop = bpy.props.FloatProperty(
name="My Float",
description="Sample float property to print to user",
default=3.1415,
min=0.0,
max=10.0,
precision=4)
bpy.types.Scene.my_bool_prop = bpy.props.BoolProperty(
name="My Bool",
description="Sample boolean property to print to user",
default=True)
bpy.types.Scene.my_int_vector_prop = bpy.props.IntVectorProperty(
name="My Int Vector",
description="Sample integer vector property to print to user",
default=(1, 2, 3, 4),
subtype='NONE',
size=4)
bpy.types.Scene.my_float_vector_prop = bpy.props.FloatVectorProperty(
name="My Float Vector",
description="Sample float vector property to print to user",
default=(1.23, 2.34, 3.45),
subtype='XYZ',
size=3,
precision=2)
bpy.types.Scene.my_bool_vector_prop = bpy.props.BoolVectorProperty(
name="My Bool Vector",
description="Sample bool vector property to print to user",
default=(True, False, True),
subtype='XYZ',
size=3)
@classmethod
def unregister(cls):
print("Unregistered class: %s " % cls.bl_label)
del bpy.types.Scene.encouraging_message
# Simple button in Tools panel
class SimplePanel(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_category = "Simple Addon"
bl_label = "Call Simple Operator"
bl_context = "objectmode"
def draw(self, context):
self.layout.operator("object.simple_operator",
text="Print Encouraging Message")
self.layout.prop(context.scene, 'encouraging_message')
self.layout.prop(context.scene, 'my_int_prop')
self.layout.prop(context.scene, 'my_float_prop')
self.layout.prop(context.scene, 'my_bool_prop')
self.layout.prop(context.scene, 'my_int_vector_prop')
self.layout.prop(context.scene, 'my_float_vector_prop')
self.layout.prop(context.scene, 'my_bool_vector_prop')
@classmethod
def register(cls):
print("Registered class: %s " % cls.bl_label)
# Register properties related to the class here.
@classmethod
def unregister(cls):
print("Unregistered class: %s " % cls.bl_label)
Listing 5-3.Exploring Scene Properties
可用bpy.props.*
变量列表见表 5-4 。更多信息参见bpy.props
的 API 文档页面。到目前为止,我们还没有涉及EnumProperty
、CollectionProperty
或PointerProperty
。我们将在本章后面介绍EnumProperty
,我们将在第七章中介绍关于高级附加功能的CollectionProperty
。
表 5-4。
Available Blender Properties
| `BoolProperty` | `EnumProperty` | `IntProperty` | `StringProperty` | | `BoolVectorProperty` | `FloatProperty` | `IntVectorProperty` | | | `CollectionProperty` | `FloatVectorProperty` | `PointerProperty` | |属性声明的参数通常很简单,其中许多参数在不同的属性之间共享。最值得注意的是:
-
default=
是长度等于指定默认值的大小的值或元组。 -
name=
是将出现在 GUI 输入字段左侧的值。 -
description=
是当用户将光标悬停在 GUI 元素上时显示的字符串。 -
precision=
指定任何浮点属性显示的小数精度。 -
size=
指定任何矢量属性中所需的矢量大小(通常是类型Vector
、bpy_boolean
或bpy_int
)。 -
subtype=
specifies the desired display formatting string for a variable. Useful examples areXYZ
andTRANSLATION
, which will display X, Y, Z, and W ahead of your first four variables in the UI. Another notable example issubtype="COLOR"
, which will create an attractive color selection UI when added to a panel. See Listing 5-4 and Figure 5-4 for an example of the color subtype. Note that Blender uses a floating-point range of (0.0, 1.0) for colors. Tables 5-5 and 5-6 show the property and vector property subtypes.表 5-6。
Available Vector Property Subtypes
| `COLOR` | `VELOCITY` | `EULER` | `XYZ` | `NONE` | | `TRANSLATION` | `ACCELERATION` | `QUATERNION` | `COLOR_GAMMA` | | | `DIRECTION` | `MATRIX` | `AXISANGLE` | `LAYER` | |表 5-5。
Available Property Subtypes
| `PIXEL` | `PERCENTAGE` | `ANGLE` | `DISTANCE` | `UNSIGNED` | `FACTOR` | `TIME` | `NONE` |图 5-4。
Color subtype
-
min=
和max=
指定可在 GUI 中显示的极值以及可存储在变量中的极值。 -
softmin=
和softmax=
指定用于显示变量和缩放滑块的最小和最大滑块值。只要在最小值和最大值之间,仍然可以手动输入任意值。 -
接受函数作为参数。该函数在每次值更新时运行。指定的函数应该接受
self
和context
作为参数,不管它是在哪里声明的。这个函数目前还没有文档,但是表现相当好。
bpy.types.Scene.my_color_prop = bpy.props.FloatVectorProperty(
name="My Color Property",
description="Returns a vector of length 4",
default=(0.322, 1.0, 0.182, 1.0),
min=0.0,
max=1.0,
subtype='COLOR',
size=4)
Listing 5-4.Using the Color Subtype
精确选择附加示例
至此,我们已经充分讨论了 Blender Python API 概念,可以开始构建有效的附加组件了。对于我们的第一个真正的附加组件,我们将参数化第三章中声明的ut.act.select_by_loc()
函数,以在编辑模式下实现精确的组选择。
在我们开始之前,请确保从 http://blender.chrisconlan.com/ut.py
下载章节 3 的ut.py
的迭代。我们将在我们的附加组件中导入它。社区使用了一些不同的协议来管理附加组件中的自定义导入。我们将讨论一个用于管理来自单级目录的自定义导入的通用协议。换句话说,我们将导入与主脚本位于同一目录中的定制模块。
我们附加组件的代码概述
我们概述了构建附加组件的步骤,从开发到部署和共享:
- 创建主脚本,并在 Blender 的文本编辑器中将其命名为
__init__.py
。将清单 5-1 中的附加模板复制到这个脚本中。 - 创建第二个脚本,在 Blender 的文本编辑器中将其命名为
ut.py
。将http://blender.chrisconlan.com/ut.py
处的 Python 模块复制到这个脚本中。 - 为我们的新附加组件修改
bl_info
。 - 添加自定义模块导入协议。参见从
if "bpy" in locals():
开始的清单 5-5 。很简单,为了测试我们是部署模式还是开发模式,我们检查bpy
是否在当前名称空间中。注意这个协议依赖于脚本中跟在它后面的import bpy
。如果我们在这个协议之前import bpy
,那么locals()
中的bpy
将总是True
,使其无效。当附加组件被加载到 Blender 中时,或者当它被部署时,这个协议将表现良好。在 Blender 文本编辑器中开发时,我们将正常导入自定义模块。- 如果
bpy
在脚本的这一点上在名称空间中,我们之前已经加载了附加组件及其依赖模块。在这种情况下,使用importlib.reload()
重新加载对象。 - 如果此时
bpy
不在名称空间中,那么我们将第一次加载附加组件。导入模块,假设它位于文件系统中与__init__.py
相同的目录中。为了从与主脚本相同的目录中导入,我们使用from . import custommodule
。Note This protocol depends on
- 如果
- 正常导入任何原生 Blender 和/或原生 Python 模块。
- 声明我们的核心操作类,
SelectByLocation
。我们将用可感知的输入参数化ut.act.select_by_loc()
作为场景属性。- 使用
bpy.props.FloatVectorProperty
注册边界框。 - 使用
bpy.props.EnumProperty
注册选择模式和坐标系的菜单。有关这些参数的解释,请参见第三章中的列表 3-8 至 3-10 。
- 使用
- 声明我们的核心面板类,
XYZSelect
。这里我们将整理与operator
相关的按钮和参数。在这种情况下,默认的菜单布局看起来非常直观。仅当模式为编辑模式时,声明poll()
classmethod 以返回True
。 - 实现安全和详细的注册,如清单 5-1 所示。
bl_info = {
"name": "XYZ-Select",
"author": "Chris Conlan",
"location": "View3D > Tools > XYZ-Select",
"version": (1, 0, 0),
"blender": (2, 7, 8),
"description": "Precision selection in Edit Mode",
"category": "3D View"
}
### Use these imports to during development ###
import ut
import importlib importlib.reload(ut)
### Use these imports to package and ship your add-on ###
# if "bpy" in locals():
# import importlib
# importlib.reload(ut)
# print('Reloaded ut.py')
# else:
# from . import ut
# print('Imported ut.py')
import bpy import os import random
# Simple Operator with Extra Properties
class xyzSelect(bpy.types.Operator):
bl_idname = "object.xyz_select"
bl_label = "Select pieces of objects in Edit Mode with bounding boxes"
def execute(self, context):
scn = context.scene
output = ut.act.select_by_loc(lbound=scn.xyz_lower_bound,
ubound=scn.xyz_upper_bound,
select_mode=scn.xyz_selection_mode,
oords=scn.xyz_coordinate_system)
print("Selected " + str(output) + " " + scn.xyz_selection_mode + "s")
return {'FINISHED'}
@classmethod
def register(cls):
print("Registered class: %s " % cls.bl_label)
bpy.types.Scene.xyz_lower_bound = bpy.props.FloatVectorProperty(
name="Lower",
description="Lower bound of selection bounding box",
default=(0.0, 0.0, 0.0),
subtype='XYZ',
size=3,
precision=
2
)
bpy.types.Scene.xyz_upper_bound = bpy.props.FloatVectorProperty(
name="Upper",
description="Upper bound of selection bounding box",
default=(1.0, 1.0, 1.0),
subtype='XYZ',
size=3,
precision=2
)
# Menus for EnumProperty's
selection_modes = [
("VERT", "Vert", "", 1),
("EDGE", "Edge", "", 2),
("FACE", "Face", "", 3),
]
bpy.types.Scene.xyz_selection_mode = \
bpy.props.EnumProperty(items=selection_modes, name="Mode")
coordinate_system = [
("GLOBAL", "Global", "", 1),
("LOCAL", "Local", "", 2),
]
bpy.types.Scene.xyz_coordinate_system = \
bpy.props.EnumProperty(items=coordinate_system, name="Coords")
@classmethod
def unregister(cls):
print("Unregistered class: %s " % cls.bl_label)
del bpy.context.scene.xyz_coordinate_system
del bpy.context.scene.xyz_selection_mode
del bpy.context.scene.xyz_upper_bound
del bpy.context.scene.xyz_lower_bound
# Simple button in Tools panel
class xyzPanel(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_category = "XYZ-Select"
bl_label = "Select by Bounding Box"
@classmethod
def poll(self, context):
return context.object.mode == 'EDIT'
def draw(self, context):
scn = context.scene
lay = self.layout
lay.operator('object.xyz_select', text="Select Components")
lay.prop(scn, 'xyz_lower_bound')
lay.prop(scn, 'xyz_upper_bound')
lay.prop(scn, 'xyz_selection_mode')
lay.prop(scn, 'xyz_coordinate_system')
@classmethod
def register(cls):
print("Registered class: %s " % cls.bl_label)
@classmethod
def unregister(cls):
print("Unregistered class: %s " % cls.bl_label)
def register():
# bpy.utils.register_module(__name__)
bpy.utils.register_class(xyzSelect)
bpy.utils.register_class(xyzPanel)
print("%s registration complete\n" % bl_info.get('name'))
def unregister():
# bpy.utils.unregister_class(xyzPanel)
# bpy.utils.unregister_class(xyzSelect)
bpy.utils.unregister_module(__name__)
print("%s unregister complete\n" % bl_info.get('name'))
if __name__ == "__main__":
try:
unregister()
except Exception as e:
print(e)
pass
register()
Listing 5-5.XYZ-Select Add-On
参见图 5-5 使用这个插件精确扭曲一个 icosphere 的例子。
图 5-5。
Color subtype
我们在这个例子中引入了两个新概念——poll()
class method 和EnumProperty
变量。我们接下来解释这两者。
poll()类方法
poll()
classmethod 是一个通常放在面板声明中的bl_*
变量之后的函数。每当 3D 视口更新时,将调用该函数来确定是否显示面板。
如果函数返回任何非空值,面板将显示。尽管任何非空值都足够,但返回一个布尔值被认为是最佳做法。回想一下,数字0
、空字符串和False
在 Python 中都被认为是 null。
在我们的插件中,如果用户处于编辑模式,我们只需返回True
,如下所示:
# poll function for edit-mode-only panels
@classmethod
def poll(self, context):
return context.object.mode == 'EDIT'
EnumProperty 变量
bpy.props.EnumProperty
类是我们通过 API 显示下拉菜单的方式。它由元组列表实例化,其中元组中的每个元素表示一个 Blender 数据值。该模式如下所示:
my_enum_list = [
("python_1", "display_1", "tooltip_1", "icon_1", 'number_1),
("python_2", "display_2", "tooltip_2", "icon_2", 'number_2),
# etc ...
("python_n", "display_n", "tooltip_n", "icon_n", 'number_n)
]
这直接来自 API 文档:
- 第一个参数是 Python 中
bpy.context.scene.my_enum_list
返回的值。 - 第二个参数是 GUI 菜单中显示的值。
- 第三个值是 GUI 菜单中显示的工具提示。它可以是空字符串。
- (可选)整数或字符串标识符,内部使用,由
bpy.types.UILayout.icon
使用。 - (可选)存储在文件数据中的唯一值,当第一个参数可能是动态的时使用。
准备我们的附加组件进行分发
要准备我们的插件进行分发,请按照下列步骤操作:
- 按照注释中的说明取消对
import
行的注释。 - 将脚本还原为显式注册和显式注销。
- (可选)测试完附加组件后,删除详细的打印语句。这纯粹是为了避免弄乱最终用户的终端。
- 替换以下文件层次中的模块,并将其压缩为一个
.zip
文件。
xyz-select/
| __init__.py
\ ut.py
要安装我们的附加组件,导航到标题菜单➤文件➤用户首选项➤附加组件➤从文件安装。在那里,选中和取消选中复选框以启用和禁用附加组件。这将触发__init__.py
中的register()
和unregister()
方法。注册应该成功,没有错误。
要直接下载压缩插件,请前往 http://blender.chrisconlan.com/xyz-select.zip
。
结论
在下一章中,我们将讨论用于在 3D 视口中可视化数据的blf
和bgl
模块。在第七章中,我们介绍了先进的附加开发概念。
六、bgl
和blf
模块
bgl
模块是 3D 视口和 Blender 游戏引擎中 Blender 常用的 OpenGL 函数的包装器。OpenGL(开放图形库)是一种开源的低级 API,用于无数的 3D 应用中,以利用硬件加速计算。
对于那些已经熟悉 OpenGL 读者来说,bgl
文档看起来很熟悉。bgl
模块本身旨在模仿 OpenGL 2.1 的调用结构和逐帧渲染风格。
在通读bgl
文档时,我们注意到许多高级概念,如缓冲操作、面剔除和光栅化。幸运的是,对于 Blender Python 程序员来说,3D 视口已经可以管理这些操作了。我们更关心用额外的信息来标记 3D 视口,以帮助用户理解他的模型。本章主要关注用bgl
绘图。
blf
模块是用于显示文本和绘制字体的一小组函数。它与bgl
密切相关,在没有它的例子中很少被提及。Blender Python 开发人员通常将bgl
和blf
模块结合起来制作测量工具,用bgl
画线,用blf
显示他们的测量结果。我们在本章中正是这样做的。
请注意,这些模块通常出现在带有bge
(Blender 游戏引擎)模块的示例中。我们将不会在 Blender 游戏引擎中工作,所以这些脚本将不会运行,并且导入bge
的尝试将会失败。我们将绘图限制在三维视口中。
还要注意的是,在 Blender 2.80+中,bgl
模块被设置为替换或大部分重建。这一章很可能是本文发布后的第一个更新。
瞬时绘图
bgl
和blf
模块不能像其他 Blender Python 模块那样被教授。当这些模块中的任何一个在 3D 视口中绘制线条或字符时,它仅在单个帧中可见。因此,我们不能像在其他模块中那样在交互式控制台中试验它。我们在交互式控制台中执行的功能可能会正确执行,但我们将无法在 3D 视口中查看结果。
为了有效地使用bgl
和blf
模块,我们必须在一个处理函数中使用它们,该函数被设置为在每次帧改变时更新。因此,我们从使用非 OpenGL 概念的处理程序示例开始。
处理程序概述
本节给出了使用bpy.app.handlers
的处理程序的例子。这不是我们在处理bgl
和blf
时最终会用到的子模块,但是它对于学习 Blender 中的处理程序是有指导意义的。
时钟示例
处理程序是设置为每次事件发生时运行的函数。为了实例化一个处理程序,我们声明一个函数,然后将它添加到 Blender 中一个可能的处理程序列表中。在清单 6-1 中,我们创建了一个用当前时间修改文本网格文本的函数。然后我们将该函数添加到bpy.app.handlers.scene_update_pre
中,以表明我们希望它在 3D 视窗更新和显示之前运行。
结果是在 3D 视口中出现一个时钟。实际上,它是一个每秒更新多次的文本网格。这个例子并不安全,也不完全可靠,但只要我们将对象保留在场景中并命名为MyTextObj
,我们就可以添加和编辑其他对象,时钟在后台运行。结果见图 6-1 。
图 6-1。
Result of the Blender clock handler example Note
时钟的行为不是记录的行为,可能会随着 Blender 的未来版本而改变。具体来说,Blender 打算改变他们称为帧改变的内容。目前,帧变化似乎是瞬间和持续发生的。
Blender 的官方文档给出了传递给处理程序的唯一参数是一个哑元的例子。处理函数应该像传统的 Python lambdas 一样处理,除了需要一个伪参数作为第一个参数。我们传递的是函数本身而不是函数的输出,传递时会创建一个新的函数未命名实例。在为处理程序创建了这个未命名的函数之后,我们不能轻易访问它。
import bpy import datetime
# Clear the scene bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete()
# Create an object for our clock bpy.ops.object.text_add(location=(0, 0, 0)) bpy.context.object.name = 'MyTextObj'
# Create a handler function
def tell_time(dummy):
current_time = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]
bpy.data.objects['MyTextObj'].data.body = current_time
# Add to the list of handler functions "scene_update_pre"
bpy.app.handlers.scene_update_pre.append(tell_time)
Listing 6-1.Blender Clock Handler Example
管理处理程序
在bpy.app.handlers
的例子中,我们可以编辑各种函数列表来管理我们的处理程序。这些列表实际上是类型为list
的 Python 类,我们可以对它们进行操作。我们可以使用list
类方法,如append()
、pop()
、remove()
和clear()
来管理我们的处理函数。参见清单 6-2 中一些有用的例子。
# Will only work if 'tell_time' is in scope
bpy.app.handlers.scene_update_pre.remove(tell_time)
# Useful in development for a clean slate
bpy.app.handlers.scene_update_pre.clear()
# Remove handler at the end of the list and return it
bpy.app.handlers.scene_update_pre.pop()
Listing 6-2.Managing Handler Lists
处理程序的类型
在清单 6-1 中,我们使用bpy.app.handlers.scene_update_pre
在每次更新前根据内部变量修改网格。表 6-1 详述了bpy.app.handlers
中出现在官方文件中的处理程序类型。
表 6-1。
Types of Handlers
| 处理者 | 呼吁 | | --- | --- | | `frame_change_post` | 渲染或回放过程中帧改变后 | | `frame_change_pre` | 渲染或回放期间帧改变之前 | | `render_cancel` | 取消渲染作业 | | `render_complete` | 完成渲染作业 | | `render_init` | 初始化渲染作业 | | `render_post` | 渲染后 | | `render_pre` | 渲染前 | | `render_stats` | 打印渲染统计 | | `render_write` | 在渲染中写入帧后 | | `load_post` | 加载. blend 文件后 | | `load_pre` | 加载. blend 文件之前 | | `save_post` | 保存. blend 文件后 | | `save_pre` | 在保存. blend 文件之前 | | `scene_update_post` | 在更新场景数据(例如,3D 视口)之后 | | `scene_update_pre` | 在更新场景数据(例如,3D 视口)之前 | | `game_pre` | 启动游戏引擎 | | `game_post` | 结束游戏引擎 |表 6-1 中有一些功能重叠,并且不是每个处理程序的行为都与预期一致。例如,在清单 6-1 中使用scene_update_post
而不是scene_update_pre
根本不起作用。鼓励读者进行试验,以确定哪一个符合他们的需求。
持久处理程序
如果我们想让处理程序在加载一个.blend
文件后保持不变,我们可以添加@persistent
装饰器。通常,当加载一个.blend
文件时,处理程序被释放,所以像bpy.app.handlers.load_post
这样的处理程序需要这个装饰器。清单 6-3 在加载一个.blend
文件后使用@persistent
装饰器打印文件诊断。
import bpy
from bpy.app.handlers import persistent
@persistent
def load_diag(dummy):
obs = bpy.context.scene.objects
print('\n\n### File Diagnostics ###')
print('Objects in Scene:', len(obs))
for ob in obs:
print(ob.name, 'of type', ob.type)
bpy.app.handlers.load_post.append(load_diag)
# After reloading startup file:
#
# ### File Diagnostics ###
# Objects in Scene: 3
# Cube of type MESH
# Lamp of type LAMP
# Camera of type CAMERA
Listing 6-3.Printing File Diagnostics on Load
blf 和 bgl 中的处理程序
现在我们已经对处理程序有了基本的了解,我们将详细说明如何使用 OpenGL 工具直接在 3D 视口中进行绘制。用于在 3D 视口中绘图的处理程序不是bpy.app.handlers
的一部分,而是bpy.types.SpaceView3D
的未记录成员函数。为了理解这些成员函数,我们减少了其他开发人员使用它们的真实例子。
清单 6-4 展示了如何使用bgl
和blf
在原点绘制一个对象的名称。
import bpy
from bpy_extras import view3d_utils import bgl
import blf
# Color and font size of text
rgb_label = (1, 0.8, 0.1, 1.0)
font_size = 16
font_id = 0
# Wrapper for mapping 3D Viewport to OpenGL 2D region
def gl_pts(context, v):
return view3d_utils.location_3d_to_region_2d(
context.region,
context.space_data.region_3d,
v)
# Get the active object, find its 2D points, draw the name
def draw_name(context):
ob = context.object
v = gl_pts(context, ob.location)
bgl.glColor4f(*rgb_label)
blf.size(font_id, font_size, 72)
blf.position(font_id, v[0], v[1], 0)
blf.draw(font_id, ob.name)
# Add the handler
# arguments:
# function = draw_name,
# tuple of parameters = (bpy.context,),
# constant1 = 'WINDOW',
# constant2 = 'POST_PIXEL'
bpy.types.SpaceView3D.draw_handler_add(
draw_name, (bpy.context,), 'WINDOW', 'POST_PIXEL')
Listing 6-4.Drawing the Name of an Object
在文本编辑器中运行清单 6-4 将允许您查看在其原点绘制的活动对象的名称。
用bpy.types.SpaceView3D
创建的处理程序不像在bpy.app.handlers
中那样容易访问,默认情况下是持久的。除非我们创建更好的控件来打开和关闭这些处理程序,否则我们将不得不重启 Blender 来分离这个处理程序。在下一节中,我们将这个处理程序放在一个附加组件中,这个附加组件允许我们用一个按钮来打开和关闭它。此外,我们将处理程序存储在一个bpy.types.Operator
中,这样在将它添加到处理程序中之后,我们就不会丢失对函数的引用。
Note
draw_handler_add()
和draw_handler_remove()
函数目前在 Blender 的官方文档中的bpy.types.SpaceView3D
中没有记载。因此,我们将根据已知的功能示例尽可能地与他们合作。
示例附加组件
这个附加组件是一个独立的脚本,因此可以通过将它复制到文本编辑器或通过用户首选项导入来运行。鼓励读者通过文本编辑器运行它,以便于实验。附件见清单 6-5 ,结果截图见图 6-2 (编辑模式下)。
图 6-2。
Drawing add-on on a cube in Edit Mode
bl_info = {
"name": "Simple Line and Text Drawing",
"author": "Chris Conlan",
"location": "View3D > Tools > Drawing",
"version": (1, 0, 0),
"blender": (2, 7, 8),
"description": "Minimal add-on for line and text drawing with bgl and blf. "
"Adapted from Antonio Vazquez's (antonioya) Archmesh." ,
"wiki_url": "http://example.com",
"category": "Development"
}
import bpy
import bmesh
import os
import bpy_extras
import bgl
import blf
# view3d_utils must be imported explicitly
from bpy_extras import view3d_utils
def draw_main(self, context):
"""Main function, toggled by handler"""
scene = context.scene
indices = context.scene.gl_measure_indices
# Set color and fontsize parameters
rgb_line = (0.173, 0.545, 1.0, 1.0)
rgb_label = (1, 0.8, 0.1, 1.0)
fsize = 16
# Enable OpenGL drawing
bgl.glEnable(bgl.GL_BLEND)
bgl.glLineWidth(1)
# Store reference to active object
ob = context.object
# Draw vertex indices
if scene.gl_display_verts:
label_verts(context, ob, rgb_label, fsize)
# Draw measurement
if scene.gl_display_measure:
if(indices[1] < len(ob.data.vertices)):
draw_measurement(context, ob, indices, rgb_line, rgb_label, fsize)
# Draw name
if scene.gl_display_names:
draw_name(context, ob, rgb_label, fsize)
# Disable OpenGL drawings and restore defaults
bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_BLEND)
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
class glrun(bpy.types.Operator):
"""Main operator, flicks handler on/off"""
bl_idname = "glinfo.glrun"
bl_label = "Display object data"
bl_description = "Display additional information in the 3D Viewport"
# For storing function handler
_handle = None
# Enable GL drawing and add handler
@staticmethod
def handle_add(self, context):
if glrun._handle is None:
glrun._handle = bpy.types.SpaceView3D.draw_handler_add(
draw_main, (self, context), 'WINDOW', 'POST_PIXEL')
context.window_manager.run_opengl = True
# Disable GL drawing and remove handler
@staticmethod
def handle_remove(self, context):
if glrun._handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(glrun._handle, 'WINDOW')
glrun._handle = None
context.window_manager.run_opengl = False
# Flicks OpenGL handler on and off
# Make sure to flick "off" before reloading script when live editing
def execute(self, context):
if context.area.type == 'VIEW_3D':
if context.window_manager.run_opengl is False:
self.handle_add(self, context)
context.area.tag_redraw()
else:
self.handle_remove(self, context)
context.area.tag_redraw()
return {'FINISHED'}
else:
print("3D Viewport not found, cannot run operator.")
return {'CANCELLED'}
class glpanel(bpy.types.Panel):
"""Standard panel with scene variables"""
bl_idname = "glinfo.glpanel"
bl_label = "Display Object Data"
bl_space_type = 'VIEW_3D'
bl_region_type = "TOOLS"
bl_category = 'Drawing'
def draw(self, context):
lay = self.layout
scn = context.scene
box = lay.box()
if context.window_manager.run_opengl is False:
icon = 'PLAY'
txt = 'Display'
else:
icon = 'PAUSE'
txt = 'Hide'
box.operator("glinfo.glrun", text=txt, icon=icon)
box.prop(scn, "gl_display_names", toggle=True, icon="OUTLINER_OB_FONT")
box.prop(scn, "gl_display_verts", toggle=True, icon='DOT')
box.prop(scn, "gl_display_measure", toggle=True, icon="ALIGN")
box.prop(scn, "gl_measure_indices")
@classmethod
def register(cls):
bpy.types.Scene.gl_display_measure = bpy.props.BoolProperty(
name="Measures",
description="Display measurements for specified indices in active mesh.",
default=True,
)
bpy.types.Scene.gl_display_names = bpy.props.BoolProperty(
name="Names",
description="Display names for selected meshes.",
default=True,
)
bpy.types.Scene.gl_display_verts = bpy.props.BoolProperty(
name="Verts",
description="Display vertex indices for selected meshes.",
default=True,
)
bpy.types.Scene.gl_measure_indices = bpy.props.IntVectorProperty(
name="Indices",
description="Display measurement between supplied vertices.",
default=(0, 1),
min=0,
subtype='NONE',
size=2)
print("registered class %s " % cls.bl_label)
@classmethod
def unregister(cls):
del bpy.types.Scene.gl_display_verts
del bpy.types.Scene.gl_display_names
del bpy.types.Scene.gl_display_measure
del bpy.types.Scene.gl_measure_indices
print("unregistered class %s " % cls.bl_label)
##### Button-activated drawing functions #####
# Draw the name of the object on its origin
def draw_name(context, ob, rgb_label, fsize):
a = gl_pts(context, ob.location)
bgl.glColor4f(rgb_label[0], rgb_label[1], rgb_label[2], rgb_label[3])
draw_text(a, ob.name, fsize)
# Draw line between two points, draw the distance
def draw_measurement(context, ob, pts, rgb_line, rgb_label, fsize):
# pts = (index of vertex #1, index of vertex #2)
a = coords(ob, pts[0])
b = coords(ob, pts[1])
d = dist(a, b)
mp = midpoint(a, b)
a = gl_pts(context, a)
b = gl_pts(context, b)
mp = gl_pts(context, mp)
bgl.glColor4f(rgb_line[0], rgb_line[1], rgb_line[2], rgb_line[3]) draw_line(a, b)
bgl.glColor4f(rgb_label[0], rgb_label[1], rgb_label[2], rgb_label[3])
draw_text(mp, '%.3f' % d, fsize)
# Label all possible vertices of
object
def label_verts(context, ob, rgb, fsize):
try:
# attempt get coordinates, will except if object does not have vertices
v = coords(ob)
bgl.glColor4f(rgb[0], rgb[1], rgb[2], rgb[3])
for i in range(0, len(v)):
loc = gl_pts(context, v[i]) draw_text(loc, str(i), fsize)
except AttributeError :
# Except attribute error to not fail on lights, cameras, etc
pass
# Convert 3D points to OpenGL-compatible 2D points
def gl_pts(context, v):
return bpy_extras.view3d_utils.location_3d_to_region_2d(
context.region,
context.space_data.region_3d,
v)
##### Core drawing functions #####
# Generic function for drawing text on screen
def draw_text(v, display_text, fsize, font_id=0):
if v:
blf.size(font_id, fsize, 72)
blf.position(font_id, v[0], v[1], 0)
blf.draw(font_id, display_text)
return
# Generic function for drawing line on screen
def draw_line(v1, v2):
if v1 and v2:
bgl.glBegin(bgl.GL_LINES)
bgl.glVertex2f(*v1)
bgl.glVertex2f(*v2)
bgl.glEnd()
if return
##### Utilities #####
# Returns all coordinates or single coordinate of object
# Can toggle between GLOBAL and LOCAL coordinates
def coords(obj, ind=None, space='GLOBAL'):
if obj.mode == 'EDIT':
v = bmesh.from_edit_mesh(obj.data).verts
elif obj.mode == 'OBJECT':
v = obj.data.vertices
if space == 'GLOBAL':
if isinstance(ind, int):
return (obj.matrix_world * v[ind].co).to_tuple()
else:
return [(obj.matrix_world * v.co).to_tuple() for v in v]
elif space == 'LOCAL':
if isinstance(ind, int):
return (v[ind].co).to_tuple()
else:
return [v.co.to_tuple() for v in v]
# Returns Euclidean distance between two 3D points
def dist(x, y):
return ((x[0] - y[0])**2 + (x[1] - y[1])**2 + (x[2] - y[2])**2)**0.5
# Returns midpoint between two 3D points
def midpoint(x, y):
return ((x[0] + y[0]) / 2, (x[1] + y[1]) / 2, (x[2] + y[2]) / 2)
##### Registration #####
def register():
"""Register objects inheriting bpy.types in current file and scope"""
# bpy.utils.register_module(__name__)
# Explicitly register objects
bpy.utils.register_class(glrun)
bpy.utils.register_class(glpanel)
wm = bpy.types.WindowManager
wm.run_opengl = bpy.props.BoolProperty(default=False)
print("%s registration complete\n" % bl_info.get('name'))
def unregister():
wm = bpy.context.window_manager
p = 'run_opengl'
if p in wm:
del wm[p]
# remove OpenGL
data
glrun.handle_remove(glrun, bpy.context)
# Always unregister in reverse order to prevent error due to
# interdependencies
# Explicitly unregister objects
# bpy.utils.unregister_class(glpanel)
# bpy.utils.unregister_class(glrun)
# Unregister objects inheriting bpy.types in current file and scope
bpy.utils.unregister_module(__name__)
print("%s unregister complete\n" % bl_info.get('name'))
# Only called during development with 'Text Editor -> Run Script'
# When distributed as plugin, Blender will directly call register()
if __name__ == "__main__":
try:
os.system('clear')
unregister()
except Exception as e:
print(e)
pass
finally:
register()
Listing 6-5.Simple Line and Text Drawing
从这里开始,我们通过引用清单 6-5 来解释使用bgl
和blf
的核心概念。我们将从最低级别的代码(核心bgl
和blf
)转移到最高级别的代码(面板和处理程序声明)。
绘制线条和文本
我们的目标是在画布上绘制线条和文本。清单 6-5 中的draw_text()
和draw_line()
函数通过将 2D 画布坐标作为输入并将信息传递给bgl
和blf
来实现这一点。
# Generic function for drawing text on screen
def draw_text(v, display_text, fsize, font_id=0):
if v:
blf.size(font_id, fsize, 72)
blf.position(font_id, v[0], v[1], 0)
blf.draw(font_id, display_text)
return
# Generic function for drawing line on screen
def draw_line(v1, v2):
if v1 and v2:
bgl.glBegin(bgl.GL_LINES)
bgl.glVertex2f(*v1)
bgl.glVertex2f(*v2)
bgl.glEnd()
return
转换到 2D 画布
这些点必须事先转换到 2D 画布的坐标系。幸运的是,bpy_extra
s 模块对此有一个实用程序。我们将bpy_extras.view3d_utils.location_3d_to_region_2d()
工具包装在一个函数中,该函数接受bpy.context
和一个 3D 点作为参数。在将任何 3D 点传递给我们的绘图函数之前,我们只需将它们转换到 2D 画布上。
# Convert 3D points to OpenGL-compatible 2D points
def gl_pts(context, v):
return bpy_extras.view3d_utils.location_3d_to_region_2d(
context.region,
context.space_data.region_3d,
v
)
声明按钮激活的绘图函数
该附加组件将做三件事:
- 使用
label_verts()
标记任何对象的顶点及其索引。 - 显示距离,并使用
draw_measurement()
在对象上的任意两个顶点之间画一条线。 - 用
draw_name()
在原点显示对象的名称。
这些函数接受传递给draw_line()
和draw_text()
的bpy.context
、对对象本身的引用、期望的索引以及颜色和字体信息。
Note
该插件执行的大多数功能都可以通过使用--debug
标志启动 Blender 或操作编辑模式的显示设置来执行。这个附件旨在为读者提供一个可以借鉴的范例。
声明主绘图函数
每次帧更新时都会执行draw_main()
功能。draw_main()
函数应该接受self
和context
。它可以接受出现在它的operator
类中的任何其他参数,我们将在下面详述,但是我们鼓励用户声明的参数通过context
作为bpy.props
对象传递。
在每一帧中,draw_main()
功能应该:
- 使用
bgl.glEnable(bgl.GL_BLEND)
启用 OpenGL 混合,并设置 OpenGL 参数。对bgl.glEnable()
的调用允许在附加组件中绘制的 OpenGL 场景与 3D 视口中的场景混合。 - 画出每一条线和每一个字符。
- 用
bgl.glDisable(bgl.GL_BLEND)
禁用 OpenGL,并重置任何 OpenGL 参数。
虽然在每一步都可以不启用或禁用 OpenGL,但鼓励确保与其他可能使用它的附加组件的合作。
用处理程序声明运算符
draw_main()
功能应该在每次帧更新时执行。为了管理操作符中的处理程序,我们使用带有函数handler_add(self, context)
和handler_remove(self, context)
的@staticmethod
装饰器。这些函数有特殊的属性,帮助它们在通过execute()
调用时与处理程序很好地交互。正如我们提到的,与这个附加组件相关的许多组件都没有文档记录,所以我们将从表面上接受它们。在operator
类之外,我们也接受与bpy.types.WindowManager
相关的行。
清单 6-5 中的glrun()
操作符类可以代表 Blender Python 中大多数(如果不是全部)支持 OpenGL 的插件。我们通常可以通过修改它外部的函数而不是修改operator
类本身来获得想要的结果。
用动态绘图声明面板
鉴于我们在第五章中对附加组件的讨论,panel 类相当简单。值得指出的是,清单 6-5 引入了组织工具self.layout.box()
,我们将在第七章中讨论。此外,我们在清单 6-5 中引入了动态面板。简而言之,draw()
类函数在每次帧更新时被调用,并且可以被动态修改而不会产生任何后果。第七章也讨论了我们如何使用它来制作更直观的附加组件。
扩展我们的 bgl 和 blf 模板
在清单 6-5 中,我们画出了物体的名称,标注了它们的顶点,并画出了从一个顶点到另一个顶点的线条和度量。使用清单 6-5 作为模板,我们可以很容易地实现更复杂和特定领域的工具。
例如,假设我们想画出每个物体到其他物体的距离。这可能有助于研究分子的原子结构或航线飞行模式。在这两种情况下,我们都关心某些物体之间的距离。清单 6-6 显示了一个我们可以添加到清单 6-5 中的函数,用于绘制提供给它的所有对象之间的距离。图 6-3 显示了结果。
图 6-3。
Drawing the distance matrix
# Draws the distance between the origins of each object supplied
def draw_distance_matrix(context, obs, rgb_line, rgb_label, fsize):
N = len(obs)
for j in range(0, N):
for i in range(j + 1, N):
a = obs[i].location
b = obs[j].location
d = dist(a, b)
mp = midpoint(a, b)
a_2d = gl_pts(context, a)
b_2d = gl_pts(context, b)
mp_2d = gl_pts(context, mp)
bgl.glColor4f(*rgb_line)
draw_line(a_2d, b_2d)
bgl.glColor4f(*rgb_label)
draw_text(mp_2d, '%.3f' % d, fsize)
# Add this to draw_main() to draw between all selected objects:
# obs = context.selected_objects
# draw_distance_matrix(context, obs, rgb_line, rgb_label, fsize)
# Add this to draw_main() to draw between all objects in scene:
# obs = context.scene.objects
# draw_distance_matrix(context, obs, rgb_line, rgb_label, fsize)
Listing 6-6.Drawing a Distance Matrix
结论
在本章中,我们讨论了如何使用处理程序bgl
和blf
在 3D 视口中实时显示数据。这是我们拥有的另一个工具,可以用来构建完整而全面的附加组件。
在下一章,我们将讨论高级附加组件。我们学习如何完全忽略文本编辑器,直接在 Blender 的文件树中构建复杂的附加组件。此外,我们还研究了一些流行的开源插件,看看它们是如何解决我们目前面临的许多开发挑战的。
七、高级附加开发
本章讨论高级插件开发中的各种主题。我们以深入了解 Blender 的一些最受欢迎的附加组件来结束这一章。
主题包括在 Blender 的文件系统中开发,在 Blender 的文本编辑器之外开发,将您的附加组件组织为传统的 Python 模块,高级面板组织,数据存储最佳实践,以及将您的附加组件提交到 Blender。
在 Blender 的文件系统中开发
至此,我们已经在 Blender 文本编辑器中开发了脚本和附加组件。我们已经处理了调整附加组件的繁琐任务,使其既可以在文本编辑器中工作,也可以作为附加组件独立工作。最终,手动修改代码以将它从开发带到部署是一种不安全的做法。我们希望确保开发中的代码与部署中的代码完全一样。
为了让开发环境模拟部署环境,我们必须直接在 Blender 的文件系统中开发。当我们提到 Blender 的文件系统时,我们指的是 Blender 根目录中的非静态应用文件。
首先,导航到您的 Blender 安装。对于 Linux 上的 64 位 Blender 2.78c,称为blender-2.78c-linux-glibc219-x86_64
。不同的操作系统有不同的名称,所以在我们的讨论中,我们将这个目录称为blender-2.78c
。附加目录位于blender-2.78c/2.78/scripts/addons
。在这个文件夹中,我们可以看到我们当前安装的所有附加组件,包括那些 Blender 发行版附带的组件。有些附加组件是单个脚本,有些是单级目录,还有一些是复杂的多级目录。
放置在此目录中的任何有效附加组件都将出现在 Blender 用户首选项中。因此,如果我们从头开始构建一个有效的附加组件,我们可以在用户首选项中激活它,而无需打开 Blender 的文本编辑器。我们已经在第五章中提到了附加组件的要求,但是我们没有将附加组件作为多级目录来讨论。参见清单 7-1 中各种类型附件的 ASCII 文件树。
### Single Scripts ###
### e.g. Node Wrangler ###
node_wrangler.py
### Single-level or Flat Directories ###
### e.g. Mesh Inset ###
mesh_inset/
|-- geom.py
|-- __init__.py
|-- model.py
|-- offset.py
'-- triquad.py
### Multi-level Directories ###
### e.g. Rigify ###
rigify
|-- CREDITS
|-- generate.py
|-- __init__.py
|-- metarig_menu.py
|-- metarigs
| |-- human.py
| |-- __init__.py
| '-- pitchipoy_human.py
|-- README
|-- rig_lists.py
|-- rigs
| |-- basic
| | |-- copy_chain.py
| | |-- copy.py
| | '-- __init__.py
| |-- biped
| | |-- arm
| | | |-- deform.py
| | | |-- fk.py
| | | |-- ik.py
| | | '--__init__.py
| | |-- __init__.py
| | |-- leg
| | | |-- deform.py
| | | |-- fk.py
| | | |-- ik.py
| | | '--__init__.py
| | '-- limb_common.py
| |-- finger.py
| |-- __init__.py
| |-- misc
| | |-- delta.py
| | '--__init__.py
| |-- neck_short.py
| |-- palm.py
| |-- pitchipoy
| | |-- __init__.py
| | |-- limbs
| | | |-- arm.py
| | | |-- __init__.py
| | | |-- leg.py
| | | |-- limb_utils.py
| | | |-- paw.py
| | | |-- super_arm.py
| | | |-- super_front_paw.py
| | | |-- super_leg.py
| | | |-- super_limb.py
| | | |-- super_rear_paw.py
| | | '-- ui.py
| | |-- simple_tentacle.py
| | |-- super_copy.py
| | |-- super_face.py
| | |-- super_finger.py
| | |-- super_palm.py
| | |-- super_torso_turbo.py
| | |-- super_widgets.py
| | '-- tentacle.py
| '-- spine.py
|-- rig_ui_pitchipoy_template.py
|-- rig_ui_template.py
|-- ui.py
'-- utils.py
Listing 7-1.Filetrees of Various Types
of Add-Ons
正如我们在清单 7-1 中看到的,用传统 Python 模块的结构以及单个脚本和平面目录来构建插件是可能的。对于一个插件来说,最好的解决方案并不取决于代码库的大小,而是取决于其功能的复杂性。Rigify 是一个需要多个目录的附加组件的很好的例子。该附件旨在装配(或准备动画)许多不同类型的网格。文件树显示了腿、手臂、触须、爪子等定制模块,每个模块都组织成一个子模块,就像biped
或limbs
一样。
在文件系统中创建附加组件
对于这个练习,我们需要一个不同于 Blender 的文本编辑器。鼓励读者打开他们最喜欢的 IDE 或文本编辑器,并创建一个新项目。在 Blender 的附加文件夹中直接创建一个名为sandbox/
的目录作为blender-2.78c/2.78/scripts/addons/sandbox/
。从那里,用清单 7-2 的内容创建一个名为__init__.py
的文件。
bl_info = {
"name": "Add-on Sandbox",
"author": "Chris Conlan",
"version": (1, 0, 0),
"blender": (2, 78, 0),
"location": "View3D",
"description": "Within-filesystem Add-on Development Sandbox",
"category": "Development",
}
def register():
pass
def unregister():
pass
# Not required and will not be called,
# but good for consistency
if __name__ == '__main__':
register()
Listing 7-2.Minimal Init File
for In-Filesystem Add-On
保存这个文件,然后打开 Blender 和导航到标题菜单➤文件➤用户偏好➤附加组件和过滤“发展”看到我们的附加组件,沙盒。结果应该如图 7-1 所示。单击复选框激活我们的附加组件,然后检查终端是否有错误。没有消息就是好消息,因为我们应该看到我们的空白附加组件实例化没有错误。
图 7-1。
Activating our sandbox
点击复选框后,在blender-2.78c/2.78/scripts/addons/sandbox/
中查找。我们看到一个名为__pycache__,
的文件夹和下面的文件树:
sandbox
|-- __init__.py
'-- __pycache__
'--__init__.cpython-35.pyc
__pycache__
文件夹是 Python 将编译后的.py
文件存储为.pyc
文件的地方。鉴于 Blender 注册附加组件的方式,__pycache__
目录中的*.pyc
文件代表附加组件的内存版本。当我们单击用户首选项中的复选框时,Blender 会确保磁盘上的 Python 源文件(例如sandbox/__init__.py
)没有改变。如果它们发生了变化,Python 将重新编译相关的__pycache__
目录,Blender 将编译后的 Python 加载到内存中。因此,虽然它们不是严格意义上的相同数据,但编译后的 Python 代表了插件的当前内存版本。这就是为什么我们可以在不影响插件的情况下实时编辑 Python 源代码。
Note
如果 Python 无法编译一个.py
文件或者 Blender 无法重新加载一个附加组件,情况就不是这样了。在这种情况下,附加组件无法成功打开,因此内存中的版本将为空白或不活动。
使用 F8 重新加载加载项
现在我们正在 Blender 文件系统中编辑我们的附加组件的源代码,我们可以重新编译附加组件来更新内存中的版本。F8 键将通过调用unregister()
,必要时重新编译.pyc
文件,然后在编译后的.pyc
文件上调用register()
来重新加载所有活动的附加组件。只需按下 F8 重新加载所有活动的附加组件,而不仅仅是我们可能正在工作的那个。这对于复杂的项目来说非常好,尤其是那些依赖于操作符和来自其他插件的函数调用的项目。通常,用这种方法编辑加载项是一种最佳实践。
当我们按下 F8 时,我们应该看到旧的内存插件的unregister()
调用的终端输出,然后是新的内存插件的register()
函数。如果插件已经更新,Blender 会在旧的插件上运行unregister()
后重新编译。如果附加组件没有更新,因此不需要重新编译,Blender 将仍然运行unregister()
函数。
下面是此类操作的控制台输出。注意,最后一行的gc.collect()
是对 Python 垃圾收集器的调用。
### F8 after updating on disk... ###
### Other modules and add-ons...
reloading addon: sandbox 1491112473.307823 1491116213.6005275 /blender-2.78c/2.78/scripts/addons/sandbox/__init__.py
module changed on disk: /blender-2.78c/2.78/scripts/addons/sandbox/__init__.py reloading...
Hello from Sandbox Registration!
gc.collect() -> 19302
### F8 without updating on disk... ###
Hello from Sandbox Unregistration!
### Other modules and add-ons...
Hello from Sandbox Registration!
gc.collect() -> 19302
重要的外卖
这可能看起来违反直觉,但是开发 Blender 附加组件的最佳实践是完全避免 Blender 文本编辑器。这引入了一些关于外部文本编辑器和 ide 的逻辑问题,我们接下来会讨论这些问题。
管理进口
回顾第五章第五章,列出第 5-5 章第五章,XYZ 选择附加组件展示了一个需要修改才能从 Blender 文本编辑器转移到附加组件的示例附加组件。清单 7-3 展示了在编辑 in-filesystem 时管理导入的正确方法。比方说,在一个平面目录中,我们让ut.py
与__init__.py
相邻。我们将导入它,如清单 7-3 所示。
if "bpy" in locals():
# Runs if add-ons are being reloaded with F8
import importlib
importlib.reload(ut)
print('Reloaded ut.py')
else:
# Runs first time add-on is loaded
from . import ut
print('Imported ut.py')
# bpy should be imported after this block of code
import bpy
Listing 7-3.Managing Imports
While Editing In-Filesystem
用于文件系统内开发的 ide
在文件系统中开发从根本上改变了我们开发 Blender Python 脚本和附加组件的方式,因为它去除了我们以前在 Blender 交互式控制台和文本编辑器中享受到的许多可访问性和模块化。尽管如此,in-filesystem 是开发已发布插件的最佳方式,我们将调整我们的工具来帮助我们完成这项工作。
我们在 Blender Python 的 IDE 中需要的工具和特性:
- Tab 补全或自动补全,通常在交互式控制台中使用 Ctrl+Space 来访问
- 使用
bpy
、bmesh
、bgl
等操作时,不产生错误标记或红色曲线。 - Python 代码突出显示,可能是特定于 Blender 的代码突出显示,我们将在后面的几类选项中讨论
轻量级(Notepad++、Gedit 和 Vim)
轻量级文本编辑器适用于简单的附加组件和脚本。一般来说,它们具有以下特征:
- 支持 Python 的语法高亮显示
- 不会为 Blender 模块创建错误标签和红色曲线
- 不支持项目管理和目录浏览
- 没有内置的制表符结束
中量级(Sublime Text、Atom 和 Spyder)
对于不想花太多时间配置 ide 的程序员来说,中量级编辑器是一个很好的默认设置。一般来说,它们与轻量级 ide 相同,但带有项目管理工具。它们具有以下特征:
- 支持 Python 的语法高亮显示
- 通常不会为 Blender 模块创建错误标签和红色曲线
- 内置项目管理和目录浏览
- 通常没有内置的 Python 制表符补全功能
- 没有内置的 Blender Python 制表符补全功能
重量级(Eclipse PyDev、PyCharm 和 NetBeans)
重量级编辑器对于已经习惯了它们的程序员来说是有好处的。它们可能需要一些配置才能很好地与 Blender Python 插件一起工作。配置选项并不总是可用。它们具有以下特征:
- 支持 Python 的语法高亮显示
- 通常为 Blender 模块创建错误标签和红色曲线
- 内置项目管理和目录浏览
- 内置 Python 的制表符补全功能
- 没有内置的 Blender Python 制表符补全功能
- 可以配置为干净地与 Blender Python 一起工作
Eclipse PyDev 在开发人员社区中很受欢迎,开发人员经常询问如何配置它以与 Blender Python 一起工作。尤其是 Eclipse,它非常讨厌在 Blender Python 模块调用上创建错误标记。已经进行了各种尝试来为它创建配置文件,但是它们没有被积极地维护。
将 Blender 编译为 Python 模块
到目前为止,缺少制表符补全的最佳一揽子解决方案(对于所有重量级 ide)是将 Blender 编译为 Python 模块。当被编译成 Python 模块时,ide 可以派生出bpy
等子模块,以建议修改和启用制表符补全。我们不在这里详述这个解决方案,因为它不能保证跨不同的操作系统工作。鼓励对这个解决方案感兴趣的 Linux 用户研究它。
将 Blender 编译成一个模块可以为你开发过程的底层控制打开更多的机会。鼓励能够将 Blender 编译为模块的用户在他的 Blender 插件 GitHub repo ( https://github.com/sybrenstuvel/random-blender-addons
)上查看 Sybren Stüvel 的 PyCharm 远程调试器插件。他的插件将底层调试控制权交给了 PyCharm 内部的开发人员。
摘要
在作者看来,对于那些对 IDE 没有特别忠诚的用户来说,中型 IDE 是 Blender Python 开发的最佳解决方案。许多开发人员努力将 Blender Python 与重量级 ide 集成在一起。满足于中等大小的 IDE 并不困难,可以参考交互式控制台和官方文档来获得 API 技巧。
外部数据的最佳实践
我们在这里转移话题,分析一些流行的插件,并评论它们如何处理外部数据。我们通过引用 Blender Python 开发者社区的例子来讨论如何最好地交付外部数据。
在第四章中,我们讨论了定义 3D 网格的各种方法。在我们的讨论中最值得注意的是。在不同软件之间传输网格、法线和纹理数据的 obj 文件格式。
Blender Python 插件通常依赖于预定义的数据。例如, BlenderAid 的资产投掷器允许用户轻松地从预定义的列表中将资产繁殖到场景中。我们讨论 Asset Flinger 和其他插件将数据导入 Blender 的方式。
使用文件交换格式
Asset Flinger 插件通过.obj
文件将网格导入 Blender。如果我们仔细检查插件的assets/
目录,我们会看到几十个.obj
文件和它们的截图。使用像.obj
这样的交换格式是将外部数据导入 Blender Python 的好方法,因为它是模块化的,是 3D 艺术家的标准。
这个插件允许用户通过添加他们自己的.obj
文件来扩展它。使用交换格式是用清晰的 Python 代码构建可扩展插件的最佳实践。清单 7-4 中的函数是将.obj
文件导入 Blender 场景所需的全部内容。
bpy.ops.import_scene.obj(filepath=myAbsoluteFilepath)
Listing 7-4.Importing OBJ Files into a Scene
正如我们将看到的,其他导入数据的方法会使您的 Python 代码变得混乱,并使其他开发人员很难就此进行协作。
使用 hhardcore python 变量
正如我们在第四章中所讨论的,一个 3D 网格需要一个最小的信息集来完全指定它,不管使用哪种文件格式。一些开发人员已经将这一知识作为 Python 变量硬编码到他们的代码中。
安东尼奥·瓦兹奎(antonioya)的 Archimesh 插件允许用户通过自定义用户界面创建和编辑建筑网格,如墙壁、窗户和门。他没有在外部以文件交换格式保存这些门和窗,而是在 Python 中将这些网格硬编码为元组列表。参见 https://github.com/Antonioya/blender/blob/master/archimesh/src/
的 Archimesh GitHub Repo 了解这方面的示例。该报告中的许多 Python 文件的末尾包含由浮点和整数表示的顶点和面数据的硬编码元组列表。
这种设计选择并非没有其动机或后果。为了创建具有任意数量的墙壁和任意数量的窗格的窗户的房间,这些 Python 变量以复杂的方式被复制、子集化和转换多次。因此,这些对象不容易相互替换。它们是专门为使用附加组件中的算法而设计的。
这里的核心 API 调用是针对bpy.data.meshes.new()
和my_mesh_object.from_pydata()
的。该插件创建一个空白网格,操纵大量 Python 数据形成对象,然后使用网格上的from_pydata()
函数实例化网格。参见清单 7-5 了解该附加组件如何操作的最小示例。清单 7-5 的底部显示了使用bpy.ops.object.add()
的另一种方法。
# Adapted from Antonio Vazquez’s Archimesh
import bpy
# Clear scene
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Manipulate Python lists of vertex and face data...
# Sample here creates a triangular pyramid
myvertex = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
myfaces = [(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]
##############################################################
# Option #1 - bpy.ops.object.add()
bpy.ops.object.add(type = 'MESH')
mainobject = bpy.context.object
mainmesh = mainobject.data
mainmesh.name = 'WindowMesh'
mainobject.name = 'WindowObject'
# Write the Python data to the mesh and update it
mainmesh.from_pydata(myvertex, [], myfaces)
mainmesh.update(calc_edges = True)
##############################################################
# WARNING: Known to cause crashes and segmentation faults in
# certain operating systems. Linux builds are safe.
# Option #2 - bpy.data.meshes.new()
mainmesh = bpy.data.meshes.new("WindowMesh")
mainobject = bpy.data.objects.new("WindowObject", mainmesh)
# Link the object to the scene, activate it, and select it
bpy.context.scene.objects.link(mainobject)
bpy.context.scene.objects.active = mainobject
mainobject.select = True
# Write the Python data to the mesh and update it
mainmesh.from_pydata(myvertex, [], myfaces)
mainmesh.update(calc_edges = True)
##############################################################
Listing 7-5.Creating Meshes with from_pydata()
通读 Archimesh 源代码,我们可以看到清单 7-5 中的一个简单的例子是如何演变成能够程序化地生成架构模型的东西的。对大量数据进行硬编码可能不是最 Pythonic 化的过程化生成方法,但它在 Archimesh 中得到了很好的应用。可以认为硬编码是不必要的,数据可以很容易地存储在外部文件中,同时仍然允许使用from_pydata()
。
原语的算法操作
将网格数据引入 Blender 的最后一种方法是图元的算法操作。在这种情况下,基本体是指默认情况下三维视口标题➤添加中的对象。例如,可以通过算法调用平面上的编辑模式操作,将它们转换成窗口的详细模型。通过不断地细分、平移和挤压一个平面,我们可以得到一个复杂的窗户模型。当我们这样做时,算法成为网格的描述符,它可以被修改以创建不同的网格变体。
当我们编写算法过程来创建网格时,它们几乎天生就是模块化的。例如,如果我们创建一个算法来建造一个有 20 根宽 6 英寸的柱子的栅栏,那么它自然会扩展到一个有 n 根宽 w 的柱子的算法。
参见清单 7-6 中算法生成迷宫的示例。我们可以调整maze_size
、maze_height
、fp
和buf
来改变迷宫的建造方式。脚本中有很多地方我们可以自定义,以进一步改变迷宫的生成方式。这就是程序生成的本质。参数化自然而然就来了。输出示例见图 7-2 。注意,这需要 http://blender.chrisconlan.com/ut.py
处的ut.py
模块可用。
图 7-2。
Randomly generated maze
import bpy
import ut
import random
# Clear scene, must be in object mode
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# size of maze
maze_size = 20
# height of maze
maze_height = 1.0
# Create NxN plane
bpy.ops.mesh.primitive_plane_add(radius = maze_size/ 2, location=(0, 0, 0.1))
# Subdivide and deselect mesh
bpy.ops.object.mode_set(mode='EDIT'))
bpy.ops.mesh.subdivide(number_cuts=maze_size - 1)
bpy.ops.mesh.select_all(action='DESELECT')
# Set starting point
v = [-maze_size / 2, -maze_size / 2]
# Stop iterating if point strays buf away from plane
buf = 5
b = [-maze_size / 2 - buf, maze_size / 2 + buf]
# Probability of point moving forward
fp = 0.6
while b[0] <= v[0] <= b[1] and b[0] <= v[1] <= b[1]:
# Select square in front of v
ut.act.select_by_loc(lbound=(v[0] - 0.5, v[1] - 0.5, 0),
ubound=(v[0] + 1.5, v[1] + 1.5, 1),
select_mode='FACE', coords='GLOBAL',
additive=True)
# Returns 0 or 1
ind = random.randint(0, 1)
# Returns -1 or 1 with probability 1 - fp or fp
dir = (int(random.random() > 1 - fp) * 2) - 1
# Adjusts point
v[ind] += dir
bpy.ops.mesh.select_all(action='INVERT')
bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={"value": (0, 0, maze_height),
"constraint_axis": (False, False, True)}
)
bpy.ops.object.mode_set(mode = 'OBJECT')
Listing 7-6.Algorithmic Manipulation of a Plane, Random Maze
清单 7-6 使用随机性和算法操作来生成一个对象。还应该注意的是,算法操作通常用于生成确定性对象。
摘要
作为一种最佳实践,作者认为应该避免硬编码 Python 变量,而采用其他两种方法:外部交换文件和算法操作。应该避免硬编码,主要是因为外部交换文件是它的最佳替代品。通过读取交换文件并将其数据保存在 Python 变量中,可以实现硬编码的所有好处。
实际上,作者认为,在不需要大量参数化的地方,应该使用外部交换文件来代替算法操作。实际上任何对象都可以用这两种方法获得,但是在参数化是第二个考虑的情况下,算法操作可能变得过于复杂(没有好处)。例如,如果我们需要一个非常详细的窗口(1000 多个顶点),而我们唯一想要参数化的是它的大小,那么用算法生成这个窗口将会浪费开发时间。这里的首选方法是从外部交换文件加载窗口,并使用 Blender 的工具调整其大小。
相反,当外部交换文件不够用时,很容易识别。如果附加组件的最初目标是参数化网格,那么选择算法操作几乎总是最好的。
高级面板创建
我们以高级面板创建的讨论来结束本章。bpy.types.Panel
类有一些有用的类方法来组织面板上的按钮。在这次讨论中,我们使用第五章中的附加模板。本次讨论使用的版本可在 http://blender.chrisconlan.com/addon_template.py
下载。
为了解释高级面板定制,我们使用已经在模板中注册的属性和操作符。换句话说,我们只关注SimplePanel
类的draw()
函数。
小组组织
我们已经讨论过如何分别调用operator()
和prop()
向画布添加按钮和特定类型的 GUI 元素。根据我们目前介绍的内容,读者只能在他们的面板中创建垂直堆叠的按钮和属性列表。清单 7-7 展示了如何使用组织功能来定制面板。结果如图 7-3 所示。
图 7-3。
Experimenting with panel functions
# Simple button in Tools panel
class SimplePanel(bpy.types.Panel)
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_category = "Simple Addon"
bl_label = "Call Simple Operator"
def draw(self, context):
# Store reference to context.scene
scn = context.scene
# Store reference to self.layout
lay = self.layout
# Create box
box = lay.box()
box.operator("object.simple_operator", text="Print #1") box.prop(scn, 'encouraging_message')
# Create another box
box = lay.box()
# Create a row within it
row = box.row()
# We can jam a few things on the same row
row.operator("object.simple_operator", text="Print #2")
row.prop(scn, 'encouraging_message')
# Create yet another box
box = lay.box()
# Create a row just for a label
row = box.row()
row.label('There is a split row below me!')
# Create a split row within it
row = box.row()
splitrow = row.split(percentage=0.2)
# Store references to each column of the split row
left_col = splitrow.column()
right_col = splitrow.column()
left_col.operator("object.simple_operator", text="Print #3")
right_col.prop(scn, 'encouraging_message')
# Throw a separator in for white space...
lay.separator()
# We can create columns within rows...
row = lay.row()
col = row.column()
col.prop(scn, 'my_int_prop')
col.prop(scn, 'my_int_prop')
col.prop(scn, 'my_int_prop')
col = row.column()
col.prop(scn, 'my_float_prop')
col.label("I'm in the middle of a column")
col.prop(scn, 'my_float_prop')
# Throw a few separators in...
lay.separator()
lay.separator()
lay.separator()
# Same as above but with boxes...
row = lay.row()
box = row.box()
box.prop(scn, 'my_int_prop') box.label("I'm in the box, bottom left.") box = row.box()
box.prop(scn, 'my_bool_prop') box.operator("object.simple_operator", text="Print #4")
Listing 7-7.Organizing Panels
bpy.types.Panel
的核心组织职能是box()
、row()
、column()
、separator()
和label()
。这五个功能中的每一个都可以嵌套在box()
、row()
或column()
中,以实现更细粒度的组织。总的来说,这是一个非常直观的 GUI 开发工具包。它使得构建美观的 GUI 变得容易。
Note
Blender 的 GUI 也是用这些工具构建的。如果您对如何复制 GUI 元素感兴趣,右键单击它并选择 Edit Source 来查看它的bpy.types.Panel
类声明。
面板图标
环顾 Blender GUI,我们注意到按钮左侧有许多不同的图标。Blender 内置了超过 550 个图标,我们可以在自己的按钮旁边使用所有这些图标。按钮由字符串表示,我们将通过icon=
参数将字符串传递给prop()
函数。在撰写本文时,对可用图标最全面的参考是 Blender 附带的图标插件。激活它后,在 Blender 文本编辑器中按 Ctrl+F 来查看属性面板,它将位于底部。清单 7-8 展示了我们如何在操作符旁边的面板中绘制图标。结果如图 7-4 所示。
图 7-4。
Experimenting with panel icons
class SimplePanel(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_category = "Simple Addon"
bl_label = "Call Simple Operator"
def draw(self, context):
# Store reference to context.scene
scn = context.scene
# Store reference to self.layout
lay = self.layout
# Create a row within it
row = lay.row()
row.operator("object.simple_operator", text="#1", icon='OBJECT_DATA')row.operator("object.simple_operator", text="#2", icon='WORLD_DATA')row.operator("object.simple_operator", text="#3", icon='LAMP_DATA')
row = lay.row()
row.operator("object.simple_operator", text="#4", icon='SOUND')row.operator("object.simple_operator", text="#5", icon='MATERIAL')row.operator("object.simple_operator", text="#6", icon='ERROR')
row = lay.row()
row.operator("object.simple_operator", text="#7", icon='CANCEL')row.operator("object.simple_operator", text="#8", icon='PLUS')row.operator("object.simple_operator", text="#9", icon='LOCKED')
row = lay.row()
row.operator("object.simple_operator", text="#10", icon='HAND')row.operator("object.simple_operator", text="#11", icon='QUIT')row.operator("object.simple_operator", text="#12", icon='GAME')
row = lay.row()
row.operator("object.simple_operator", text="#13", icon='PARTICLEMODE')row.operator("object.simple_operator", text="#14", icon='MESH_MONKEY')row.operator("object.simple_operator", text="#15", icon='FONT_DATA')
row = lay.row()row.operator("object.simple_operator", text="#16", icon='SURFACE_NSPHERE')
row.operator("object.simple_operator", text="#17", icon='COLOR_RED')row.operator("object.simple_operator", text="#18", icon='FORCE_LENNARDJONES')
row = lay.row()
row.operator("object.simple_operator", text="#19", icon='MODIFIER')row.operator("object.simple_operator", text="#20", icon='MOD_SOFT')row.operator("object.simple_operator", text="#21", icon='MOD_DISPLACE')
row = lay.row()
row.operator("object.simple_operator", text="#22", icon='IPO_CONSTANT')
row.operator("object.simple_operator", text="#23", icon='GRID')row.operator("object.simple_operator", text="#24", icon='FILTER')
Listing 7-8.
Panel Icons
结论
这就结束了我们对高级附加组件的讨论。本指南绝不是全面的,因为当涉及到附加开发时,有许多地方和可能性可以探索。重要的是要记住 Blender GUI 本身是建立在我们已经讨论过的 Python 类之上的,所以我们看到的每一个功能都可以被复制。
本章关于附加组件组织的背景知识应该让读者更容易理解其他开发者的源代码。Blender 是一个开源平台,鼓励用户共享代码和相互学习。鼓励读者复制和修改其他开发者的作品,然后分享他们的作品供他人学习。
下一章以纹理和渲染的处理来结束这篇文章。
八、纹理和渲染
到目前为止,我们已经将代码示例限制为在 Blender 中创建网格和附加组件。对于 3D 艺术家和动画师来说,3D 建模的目标是通过渲染图像和视频使场景变得栩栩如生。Blender Python 中的渲染实际上非常简单,通常只需要一次函数调用。为了把我们带到我们想要渲染场景的地方,我们将讨论纹理、光照和摄像机的放置。
在本章结束时,用户将能够为纹理、照明、相机放置和静态渲染创建自动化管道。虽然使用 Blender Python 渲染动画视频是可能的,但我们在这里的讨论将仅限于渲染静态图像。
纹理词汇
一般来说,有许多类型的纹理,在 Blender 中有许多额外的参数化类型。我们的第一个例子使用漫射纹理和法线贴图来说明材质在 Blender 中的作用。在我们继续之前,我们将建立一些关于纹理的新词汇。
Blender 中的影响类型
虽然这些效果在 Blender 中被归类为影响,但它们在传统上是指 3D 建模的广阔领域中的纹理类型。Blender 有自己的纹理类型,每种类型都可以采用这些影响。参见图 8-1 了解这些影响在 Blender GUI 中的位置。他们可以在➤材料➤影响的属性中找到。
图 8-1。
Influences in Blender
- 漫射纹理用于给对象着色。漫射纹理可以描述 Blender 中对象的颜色、强度、alpha 级别和透明度。为了在物体表面覆盖图像,我们使用了漫反射颜色纹理。
- 着色纹理描述了对象如何与场景中的其他对象进行交互。如果我们希望对象镜像另一个对象,将颜色发射到另一个对象上,或者将环境光投射到场景中,我们在 Blender 中指定必要的着色属性。
- 镜面纹理描述了对象对光线的反应。例如,如果我们提供一个静态模糊的图像(就像在旧电视屏幕上看到的一样)作为镜面纹理,光线会像闪亮的沙粒一样从对象上反射回来。我们可以通过指定颜色对光的反应强度和方向来微调高光贴图。
- 几何体纹理允许对象影响对象的几何外观。例如,如果我们为几何地图提供黑白条纹并指定法线地图,我们将在模型中看到 3D 山脊。值得注意的是,这些效果仅在渲染中实现,而不是在网格数据本身中实现。
Blender 中的纹理类型
虽然我们将主要使用图像纹理,Blender 有许多可定制的纹理供我们选择。这些是从图 8-2 所示的➤材料➤类型菜单中选择的。
图 8-2。
Texture types in Blender
图像和视频以及环境贴图选项可以导入图像和视频文件。剩余的纹理可以在 Blender 中参数化,以获得所需的结果。我们没有详细说明如何具体处理这些参数化纹理,因为要讨论的参数有很多。清单 8-1 解释了如何使用图像和视频类型的参数来纹理化一个对象。从这里开始,读者应该能够使用 Blender 的 Python 工具提示为任何剩余的类型复制这个过程。
添加和配置纹理
在讨论文件交换格式时,我们在第四章中提到了纹理的定义。纹理通过 uv 坐标映射到 3D 空间中的面。为了将正方形图像作为纹理映射到网格的正方形表面,我们将 uv 坐标[(0, 0), (1, 0), (0, 1), (1, 1)]
分别指定到网格的左下角、右下角、左上角和右上角。随着面的形状变得越来越复杂,实现所需纹理映射的过程也越来越复杂。接下来我们讨论将 uv 坐标映射到普通形状的方法。
加载纹理和生成 UV 贴图
由于 Blender 处理纹理导入和材质的方式,uv 贴图并不是一项简单的任务。我们必须克服一些程序上的障碍,才能在脚本中明确定义对象的 uv 坐标。一旦我们达到这一点,精确指定紫外线坐标是相当简单的。我们在清单 8-1 中举例说明。
我们在示例中使用数字 1 和 2 的样本图像,这些图像可以在 http://blender.chrisconlan.com/number_1.png
和 http://blender.chrisconlan.com/number_2.png
下载。读者可以使用这些图像或任何其他想要的图像来列出 8-1 。结果见图 8-4 。我们将在下面的章节中讨论清单 8-1 中使用的函数。
Note
运行该脚本后,通过在 3D 视口标题中选择渲染视图来查看结果,如图 8-3 所示。
图 8-3。
Selecting rendered view
图 8-4。
Explicitly mapping UV coordinates
import bpy
import bmesh
from mathutils import Color
# Clear scene
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create cube
bpy.ops.mesh.primitive_cube_add(radius = 1, location = (0, 0, 0))
bpy.ops.object.mode_set(mode = 'EDIT')
# Create material to hold textures
material_obj = bpy.data.materials.new('number_1_material')
### Begin configure the number one ###
# Path to image
imgpath = '/home/cconlan/Desktop/blender-book/ch08_pics/number_1.png'
image_obj = bpy.data.images.load(imgpath)
# Create image texture from image
texture_obj = bpy.data.textures.new('number_1_tex', type='IMAGE')
texture_obj.image = image_obj
# Add texture slot for image texture
texture_slot = material_obj.texture_slots.add()
texture_slot.texture = texture_obj
### Begin configuring the number two ###
# Path to image
imgpath = '/home/cconlan/Desktop/blender-book/ch08_pics/number_2.png'
image_obj = bpy.data.images.load(imgpath)
# Create image texture from image
texture_obj = bpy.data.textures.new('number_2_tex', type='IMAGE')
texture_obj.image = image_obj
# Add texture slot for image texture
texture_slot = material_obj.texture_slots.add()
texture_slot.texture = texture_obj
# Tone down color map, turn on and tone up normal mapping
texture_slot.diffuse_color_factor = 0.2
texture_slot.use_map_normal = True
texture_slot.normal_factor = 2.0
### Finish configuring textures ###
# Add material to current object
bpy.context.object.data.materials.append(material_obj)
### Begin configuring UV coordinates ###
bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
bm.faces.ensure_lookup_table()
# Index of face to texture
face_ind = 0
bpy.ops.mesh.select_all(action='DESELECT')
bm.faces[face_ind].select = True
# Unwrap to instantiate uv layer
bpy.ops.uv.unwrap()
# Grab uv layer
uv_layer = bm.loops.layers.uv.active
# Begin mapping...
loop_data = bm.faces[face_ind].loops
# bottom right
uv_data = loop_data[0][uv_layer].uv
uv_data.x = 1.0
uv_data.y = 0.0
# top right
uv_data = loop_data[1][uv_layer].uv
uv_data.x = 1.0
uv_data.y = 1.0
# top
left
uv_data = loop_data[2][uv_layer].uv
uv_data.x = 0.0
uv_data.y = 1.0
# bottom left
uv_data = loop_data[3][uv_layer].uv
uv_data.x = 0.0
uv_data.y = 0.0
# Change background color to white to match our example
bpy.data.worlds['World'].horizon_color = Color((1.0, 1.0, 1.0))
# Switch to object mode to add lights
bpy.ops.object.mode_set(mode='OBJECT')
# Liberally add lights
dist = 5
for side in [-1, 1]:
for coord in [0, 1, 2]:
loc = [0, 0, 0]
loc[coord] = side * dist
bpy.ops.object.lamp_add(type='POINT', location=loc)
# Switch to rendered mode to view results
Listing 8-1.Loading Textures and Generating UV Maps
纹理与 Blender 中的材料
纹理是 3D 建模中的一个广义术语。它可以指漫射纹理、颜色纹理、渐变纹理、凹凸贴图等等。值得注意的是,我们可以同时将所有这些形式的纹理映射到一个对象上。例如,屋顶上的一组木瓦可能需要图像纹理、漫反射贴图和凹凸贴图,以便在渲染时看起来逼真。
此外,现实世界材质的图像、漫反射贴图和凹凸贴图通常是专门为彼此构建的。在我们的瓦片区示例中,凹凸贴图将定义物理瓦片区之间的脊线,因为它们出现在图像纹理中。漫射贴图将进一步定义我们通常在屋顶木瓦上看到的闪亮颗粒。按照设计,表示图像和地图的文件不一定要与集合外的其他文件一起工作。这就是 Blender 中材料的动机。
Blender 中的材质是纹理相关数据的集合。它可能包括前面提到的任何图像和贴图,也可能包括其他图像和贴图。因此,我们必须首先从其组成纹理构建材质,然后将材质指定给对象。不管一个材质是由一个还是多个纹理组成,纹理数据都必须分配给这个材质。然后,必须将材质指定给对象。
这一讨论揭示了清单 8-1 中物料管理背后的动机。我们首先声明并操作所有需要的纹理,然后通过bpy.context.object.data.materials.append()
将整个材质添加到对象中。从这里,我们可以操纵整个材料的 uv 坐标。
UV 坐标和循环
清单 8-1 的后半部分访问一个我们以前没有使用过的数据端点。我们要访问的 uv 坐标数据层包含在一个 loops 对象中。可以将循环视为跟踪 3D 对象的一组顶点的 3D 多边形。循环可以跨越多个面,但必须在同一点开始和结束。当循环跨越多个面时,它们旨在捕捉相邻面的局部集合。
3D 艺术家可以使用帮助他们创建循环的高级工具。然后,这些循环帮助他们手动指定 uv 坐标。虽然我们不会在 Blender Python 中操作这些循环,但理解它们如何工作是很重要的,因为loops
数据对象位于网格本身和 uv 层之间。
幸运的是,Blender 中的循环数据对象与我们习惯使用的bmesh.faces[].verts[]
对象是一一对应的。换句话说,对于任意两个整数,f
和v
,bm.faces[f].loops[v][uv_layer].uv
访问的(u,v)坐标对应于bm.faces[f].verts[v].co
访问的(x,y,z)坐标。
值得注意的是,两个整数f
和v
可能不指定 3D 空间中的唯一点。在默认的 Blender 2.78c 立方体中,如在启动文件中出现的那样,f:v
对0:2
、3:3
和4:0
都对应于 3D 空间中的点(-1.0, -1.0, -1.0)
。当立方体被纹理化时,这些 uv 坐标通常是唯一的,因为它们都对应于纹理贴图的不同部分。
关于索引和交叉兼容性的另一个说明
当动态纹理化对象时,我们会遇到一个类似于第三章中提到的问题。在那一节中,我们注意到顶点索引的行为是可复制的,但是不可控制的,因此证明了通过特征选择作为一种解决方法的合理性(在清单 3-13 中实现)。同样的概念也适用于这里,除了我们必须与bm.faces[f].verts[v].co
一起工作,而不仅仅是与bm.verts[v].co
一起工作。
例如,假设我们想在一个立方体的顶部沿 y 轴垂直放置一个纹理。一个可能的解决方案是使用我们的ut.py
工具包中的ut.act.select_by_loc()
来根据它的位置选择立方体的顶面。从这里,我们可以用f_ind = [f.index
代替bm.faces if f.select][0]
中的f
,返回所选的人脸索引。使用面索引,我们可以将面的顶点存储为bm.faces[f_ind.verts]]
中v
的vert_vectors = v.co
,并使用该信息沿着立方体定位我们的纹理。
我们的另一个选择是违背第 [3 章“关于索引和交叉兼容性的注释”的建议,假设我们在纹理化之前知道一个对象的面顶点的位置和方向。我们通常可以预先确定这些信息,并将其硬编码到我们的纹理脚本中,就像我们在清单 8-1 中所做的那样。对于受控和内部使用,这是一个可行的选择,但对于我们将与社区共享并经过跨版本兼容性测试的代码,建议不要这样做。
基于我们到目前为止的讨论,读者应该有工具和知识来实现他们想要的动态(或非动态)纹理脚本。第三章的引用部分,以及随后的部分,与读者可能承担的任何动态纹理任务非常相似。
我们现在继续讨论 Blender 中的渲染和它的一些用途。
移除未使用的纹理和材质
我们已经讨论了在 Blender 中删除网格和对象的许多有用的功能。随着我们不断地测试脚本,我们的材质和纹理数据会在我们没有意识到的情况下很快变得混乱。Blender 会将纹理重命名为my_texture.001
、my_texture.002
等。当我们忘记删除它们时。
纹理和材质必须没有用户才能被删除。在这种情况下,用户指的是当前为其分配了对象的数量。为了删除纹理和材质,我们循环遍历我们的bpy.data.materials
和bpy.data.textures
数据块,并调用那些没有被使用的.remove()
。参见清单 8-2 了解该实施。
import bpy
mats = bpy.data.materials
for dblock in mats:
if not dblock.users:
mats.remove(dblock)
texs = bpy.data.textures
for dblock in mats:
if not dblock.users:
texs.remove(dblock)
Listing 8-2.Loading Textures and Generating UV Maps
使用 Blender 渲染进行渲染
使用 Blender 的内置渲染功能非常简单。我们介绍并解释如何在场景中定位灯光和摄像机,然后调用渲染函数来创建图像。我们的大部分讨论集中在语义和相机和灯光的辅助函数上。
添加灯光
在清单 8-1 中,我们在立方体周围添加了六盏灯,使其在 3D 视口中的 Blender 渲染视图中可见。正确使用这个视图和渲染通常需要灯光。光照是 3D 建模中一个重要的大领域。在本节中,我们将重点关注与照明相关的 Blender Python 函数,而不是美观的照明的一般实践。
在三维视口标题,我们可以导航到添加➤灯,以选择任何 Blender 的内置灯。使用 Python 工具提示,我们可以看到它们都依赖于函数bpy.ops.object.lamp_add()
,用type= parameter
决定光线的类型。我们有SUN
、POINT
、SPOT
、HEMI
和AREA
等选项。每种类型都有自己的参数集需要配置。
当谈到程序生成的照明时,我们主要关心的是位置和方向。我们将介绍一些管理布局和方向的工具。例如,要懒洋洋地照亮整个场景,我们可能希望在场景的聚集边界框周围创建点光源。此外,我们可能希望将聚光灯直接指向另一个任意放置的对象。见清单 8-3 中可能有助于程序性添加灯光的实用程序列表。我们在清单 8-3 中声明的所有函数都已经添加到我们的工具包ut.py
中,可以在 http://blender.chrisconlan.com/ut.py
下载。
参见表 8-1 了解每种类型灯的基本描述
表 8-1。
Types of Lights
| 类型 | 描述 | | --- | --- | | 要点 | 向各个方向均匀发光;旋转没有效果 | | 地点 | 向特定的方向发射光锥 | | 面积 | 从矩形区域发光;遵循朗伯分布 | | 半球的 | 类似于面积,但具有球形曲率 | | 太阳 | 在特定方向发射正交光;位置没有影响 |添加摄像机
渲染场景需要摄像机。为了程序化地添加摄像机,我们必须定位它,调整它的方向,并修改它的参数。我们将使用清单 8-3 中的函数来定位和引导摄像机以及灯光。
当程序性地生成摄像机时,我们必须解决的最大问题是确定距离和视野,以使整个场景在渲染时不会显得太小。我们将使用一些基本的三角学来解决这些问题。
视场(FoV)是从相机向外投射的一对两个角度(θ x ,θ y ,定义了一个无限延伸的四棱锥。如果在它前面没有任何东西,位于这个四棱锥内的所有东西都可以被摄像机看到。举个例子,iPhone 6 相机在风景模式下的视场角约为(63,47)度。请注意,当摄影师通俗地提到 FoV 时,他们通常只指两个角度中较大的一个。
我们必须了解 FoV,这样我们才能确保相机的放置和校准捕捉到我们想要渲染的场景。
给定一个具有 FoV (θ x ,θ y )的相机,该相机沿着并且面对具有高 h 和宽 w 的边界框的场景居中,捕获该场景所需的离场景 d 的距离是 max(d x ,d y )。对于本讨论,d x 和 d y 分别表示沿水平和垂直维度捕捉场景所需的距离。直观表示见图 8-5 。利用基本的三角学,我们得到
图 8-5。
Field of view along the y-axis
这只说明了相机指向 x 轴或 y 轴的简单情况,但它足以满足我们的目的。在清单 8-4 中,我们使用之前建立的效用函数来控制摄像机,使其能够渲染整个可视场景。
# Point a light or camera at a location specified by "target"
def point_at(ob, target):
ob_loc = ob.location
dir_vec = target - ob.location
ob.rotation_euler = dir_vec.to_track_quat('-Z', 'Y').to_euler()
# Return the aggregate bounding box of all meshes in a scene
def scene_bounding_box():
# Get names of all meshes
mesh_names = [v.name for v in bpy.context.scene.objects if v.type == 'MESH']
# Save an initial value
# Save as list for single-entry modification
co = coords(mesh_names[0])[0]
bb_max = [co[0], co[1], co[2]]
bb_min = [co[0], co[1], co[2]]
# Test and store maxima and minima
for i in range(0, len(mesh_names)):
co = coords(mesh_names[i])
for j in range(0, len(co)):
for k in range(0, 3):
if co[j][k] > bb_max[k]:
bb_max[k] = co[j][k]
if co[j][k] < bb_min[k]:
bb_min[k] = co[j][k]
# Convert to tuples
bb_max = (bb_max[0], bb_max[1], bb_max[2])
bb_min = (bb_min[0], bb_min[1], bb_min[2])
return [bb_min, bb_max]
Listing 8-3.Utilities for Lights and Cameras
渲染图像
渲染是在给定 3D 数据的情况下计算高分辨率影像和视频的过程。渲染不是即时的。当我们平移和旋转相机时,Blender 中的 3D 视口似乎会流畅地移动,渲染可能会花费相当多的时间。3D 视口是 3D 数据的即时渲染,但它并不代表与传统渲染相同的质量或清晰度水平。
在清单 8-4 中,我们使用 Blender 渲染和 OpenGL 渲染来渲染清单 8-1 的输出。此示例假设将相机定位为从场景的 yz 中心点沿 x 轴向上指向场景的中心点,这样它将捕捉整个场景。我们使用前面讨论过的公式来实现这一点。回想一下,这些等式假设了一个简单的情况,即我们沿着一个轴对准摄像机。
最终的渲染将对象直接捕获到框架中。清单 8-1 中创建的立方体的混合渲染见图 8-6 。对于 Blender 渲染,场景的摄影机用作渲染摄影机。这就是为什么知道如何在程序上设置相机的位置是很重要的。如果我们想要循环并渲染许多场景,我们需要确信场景将在帧内被捕获。
图 8-6。
Blender Render
我们还可以使用 OpenGL 渲染来渲染 3D 视口的快照。这将捕捉场景的基本特征,类似于我们在实体视图中在对象模式下看到的 3D 视口。结果如图 8-7 所示。请注意,在此视图中,我们可以看到灯光和相机,但不能看到材质。当我们调用bpy.ops.render.opengl()
时,设置view_context = True
将导致 Blender 使用 3D 视口相机(用户的视图)而不是场景相机。
图 8-7。
OpenGL rendering
### Assumes output of Listing 8-1 is in scene at runtime ###
import bpy
import bmesh
import ut
from math import pi, tan
from mathutils import Vector
# Get scene's bounding box (meshes only)
bbox = ut.scene_bounding_box()
# Calculate median of bounding box
bbox_med = ( (bbox[0][0] + bbox[1][0])/2,
(bbox[0][1] + bbox[1][1])/2,
(bbox[0][2] + bbox[1][2])/2 )
# Calculate size of bounding box
bbox_size = ( (bbox[1][0] - bbox[0][0]),
(bbox[1][1] - bbox[0][1]),
(bbox[1][2] - bbox[0][2]) )
# Add camera to scene
bpy.ops.object.camera_add(location=(0, 0, 0), rotation=(0, 0, 0))
camera_obj = bpy.context.object
camera_obj.name = 'Camera_1'
# Required for us to manipulate FoV as angles
camera_obj.data.lens_unit = 'FOV'
# Set image resolution in pixels
# Output will be half the pixelage set here
scn = bpy.context.scene
scn.render.resolution_x = 1800
scn.render.resolution_y = 1200
# Compute FoV angles
aspect_ratio = scn.render.resolution_x / scn.render.resolution_y
if aspect_ratio > 1:
camera_angle_x = camera_obj.data.angle
camera_angle_y = camera_angle_x / aspect_ratio
else:
camera_angle_y = camera_obj.data.angle
camera_angle_x = camera_angle_y * aspect_ratio
# Set the scene's camera to our new camera
scn.camera = camera_obj
# Determine the distance to move the camera away from the scene
camera_dist_x = (bbox_size[1]/2) * (tan(camera_angle_x / 2) ** -1)
camera_dist_y = (bbox_size[2]/2) * (tan(camera_angle_y / 2) ** -1)
camera_dist = max(camera_dist_x, camera_dist_y)
# Multiply the distance by an arbitrary buffer
camera_buffer = 1.10
camera_dist *= camera_buffer
# Position the camera to point up the x-axis
camera_loc = (bbox[0][1] - camera_dist, bbox_med[1], bbox_med[2])
# Set new location and point camera at median of scene
camera_obj.location = camera_loc
ut.point_at(camera_obj, Vector(bbox_med))
# Set render path
render_path = '/home/cconlan/Desktop/blender_render.png'
bpy.data.scenes['Scene'].render.filepath = render_path
# Render using Blender Render
bpy.ops.render.render( write_still = True )
# Set render path
render_path = '/home/cconlan/Desktop/opengl_render.png'
bpy.data.scenes['Scene'].render.filepath = render_path
# Render 3D viewport using OpenGL render
bpy.ops.render.opengl( write_still = True , view_context = True )
Listing 8-4.Rendering Using Blender Render and OpenGL Render
结论
本章总结了我们对 Blender Python API 的讨论。即使有许多例子,这篇文章也不是一个全面的指南。这是对 Blender 复杂性和模块化的最好证明。Blender 可以使用 Python API 进行编辑、调整、定制和扩展。这本书的作者和协助其开发的专业人士希望这些知识有助于鼓励 Blender 社区的研究和开发。