Blender Python API 教程(二)
原文:The Blender Python API
协议:CC BY-NC-SA 4.0
五、插件开发简介
本章使用 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 版本中排除的未完成的或新的附加组件。运算符和类继承(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 are XYZ
and TRANSLATION
, which will display X, Y, Z, and W ahead of your first four variables in the UI. Another notable example is subtype="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 中需要的工具和特性:
bpy
、bmesh
、bgl
等操作时,不产生错误标记或红色曲线。轻量级(Notepad++、Gedit 和 Vim)
轻量级文本编辑器适用于简单的附加组件和脚本。一般来说,它们具有以下特征:
中量级(Sublime Text、Atom 和 Spyder)
对于不想花太多时间配置 ide 的程序员来说,中量级编辑器是一个很好的默认设置。一般来说,它们与轻量级 ide 相同,但带有项目管理工具。它们具有以下特征:
重量级(Eclipse PyDev、PyCharm 和 NetBeans)
重量级编辑器对于已经习惯了它们的程序员来说是有好处的。它们可能需要一些配置才能很好地与 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 是一个开源平台,鼓励用户共享代码和相互学习。鼓励读者复制和修改其他开发者的作品,然后分享他们的作品供他人学习。
下一章以纹理和渲染的处理来结束这篇文章。
作者:绝不原创的飞龙