Blender Python API 教程(一)
Blender 界面的组件是模块化的、可拆卸的、可扩展的和全面可定制的。用户可以拖动任何窗口的右上角来修改和创建新窗口。向左拖动右上角将创建一个相同类型的新窗口向右拖动右上角将允许您超越相邻的窗口按住 Shift 键并向任意方向拖动右上角将在一个新的分离窗口中复制组件在一个可分离的窗口中创建一个 3D 视窗并复制文本编辑器是使用双屏设置的好方法。拥有两个可用的文本编辑器对于调试定制模块非常有帮助。
一、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。尽管如此,我们还是引入了这些概念,因为在调试时,意识到这些问题及其驱动因素可以避免很多麻烦。
更多推荐
所有评论(0)