可扩展的Blender插件开发
成熟的 Blender 3D 插件是令人惊奇的事情。作为 Python 和 Blender 的新手,我经常发现自己被社区中的人们创造的强大的东西弄得目瞪口呆。坦率地说,其中一些包看起来有点神奇,当自我怀疑或冒名顶替综合症的唠叨声音被打破时,很容易想到“如果有人能做出可以做X的东西就好了” 。
然后我记得,通过将好奇心和固执与良好的文档相结合,某人可以是任何人,X可以成为X、Y 和 Z。即使是困难的部分也可以弄清楚——尤其是因为所有固执和好奇的人确保 Blender 的 Python文档和stackexchange与它让我们创建的 gee whiz 图形一样好。
同样的方式,已经存在的文档和模型为从头开始编写 Blender 插件奠定了平滑的基础,在开始时为该插件提供可扩展的结构有助于展示 Python API 的各个部分如何更多地组合在一起清楚地。换句话说,它使新编写的代码比原来更好,同时也使现有代码更容易学习。
在本文的最后,我们将创建一个功能齐全且已安装的插件,它提供了一个自定义 UI 元素来将Standoff
添加到 Blender 场景中,并带有界面控件来调整创建的网格的直径和高度。
1、文件结构
考虑到这一点,这是本文末尾将存在的完整目录和文件结构,可以在开头使用mkdir
和touch
创建。那么这只是一个填空游戏。我正在调用项目DemoRack
并将其设置为我用于 Python 项目的文件夹中的顶级目录名称——它不一定必须是 Blender 特定的任何地方。
DemoRack
|-- README.md
|-- DemoRack.zip <-- will (re)compile via 'zip -r DemoRack.zip src'
|-- src
|-- |-- __init__.py
|-- |-- standoff_mesh.py <-- from Part 2, not modified in this post
|-- |-- standoff_operator.py
|-- |-- standoff_panel.py
|-- |-- standoff_props.py
这些文件中的每一个都将在一个单独的步骤中介绍,但首先,这是它们的简要描述:
- DemoRack.zip:已编译
src
,安装在 Blender 中的文件。 - __init__.py:为附加组件注册所有必要的信息和类。
- standoff_mesh.py:用于生成目标几何/网格数据的模块。
- standoff_operator.py:将提供给 UI 使用的“do-er”。
- standoff_panel.py:在 UI 元素上添加插件将......添加。
- standoff_props.py:定义
Panel
和Operator
所需的数据对象。
简而言之,每个新standoff_
模块都将包含register()
和unregister()
函数。该__init__
模块将导入这些模块,并将两种类型的函数捆绑到单个迭代器中。Blender Python 文档描述了这些函数所扮演的角色:
register
是一个仅在启用插件时运行的函数,这意味着可以在不激活插件的情况下加载模块。unregister
是一个卸载任何设置的函数,register
当插件被禁用时调用。
2、__init__.py
有了这个背景,并且因为__init__
模块中的大部分代码都与sys
和importlib
包有关,所以我将在此处包含要点,而不会试图去描述 Python 模块导入的杂草。Blender插件需要注意的具体事项是module_names
列表,声明要引入register
和unregister
函数的的文件名,以及打开的bl_info
字典。如官方插件介绍教程中所述,bl_info
包含将在“首选项”窗格中找到的所有信息:
bl_info
是包含附加元数据的字典,例如要显示在首选项附加列表中的标题、版本和作者。它还指定了运行脚本所需的最低 Blender 版本;旧版本不会在列表中显示加载项
bl_info = {
"name": "DemoRack",
"description": "Make Mini Rack Units Dynamically",
"author": "Jim O'Connor <hello@ocommaj.com>",
"version": (0, 0, 1),
"blender": (2, 90, 1),
"category": "3D View"
}
module_names = [ 'standoff_props', 'standoff_operator', 'standoff_panel' ]
import sys
import importlib
module_full_names = [ f"{__name__}.{module}" for module in module_names ]
for module in module_full_names:
if module in sys.modules:
importlib.reload(sys.modules[module])
else:
locals()[module] = importlib.import_module(module)
setattr(locals()[module], 'module_names', module_full_names)
def register():
for module in module_full_names:
if module in sys.modules:
if hasattr(sys.modules[module], 'register'):
sys.modules[module].register()
def unregister():
for module in module_full_names:
if module in sys.modules:
if hasattr(sys.modules[module], 'unregister'):
sys.modules[module].unregister()
3、standoff_props.py
该模块将是其中涉及最多的模块,但它也为其他模块提供了主干,并实现了一种可以广泛重用的模式。它依赖于导入用于将一组属性定义捆绑在一起的PropertyGroup
类型(文档),bpy.props
。一旦PropertyGroup
在 Blender 中注册,它就在 Python 可脚本化数据对象的指针和底层 C 分配的内存之间提供了一座桥梁,这些内存完成了 Blender 的繁重工作。
在standoff_props.py
类中将定义、继承bpy.types.PropertyGroup
并跟踪 3 个属性:
metric_diameter: FloatProperty(**kwargs)
height: FloatProperty(**kwargs)
mesh: PointerProperty(type=Mesh)
前两个应该是不言自明的,并且在实现中将有更多关于参数的细节。第3个PointerProperty
指向内存中的一个对象,并要求在定义时指定该对象的类型,并且它是 PropertyGroup
的子类或bpy.struct.ID
(即 Mesh)。这意味着任何将值设置为任何其他数据类型的实例的尝试(在这种情况下,任何非 bpy.types.Mesh
)都将引发错误,任何将值传递给期望任何其他数据的参数的尝试也是如此类型。
在这种情况下,mesh
的 PointerProperty
属性将用于保存在Standoff.mesh()
的返回值,并使用存储在metric_diameter
和height
后面的值进行实例化和修改。这三个属性的完整定义如下所示:
class PG_Standoff(PropertyGroup):
metric_diameter: FloatProperty(
name="Inner Diameter (Metric)",
min=2,
max=5,
step=50,
precision=1,
set=prop_methods("SET", "metric_diameter"),
get=prop_methods("GET", "metric_diameter"),
update=prop_methods("UPDATE"))
height: FloatProperty(
name="Standoff Height",
min=2,
max=6,
step=25,
precision=2,
set=prop_methods("SET", "height"),
get=prop_methods("GET", "height"),
update=prop_methods("UPDATE"))
mesh: PointerProperty(type=Mesh)
单独的set
、 get
和update
参数都指向prop_methods
函数的返回值。这些值必须是函数,参数分别为(self, value)
、(self)
和(self, context)
。这个闭包工厂可能看起来额外复杂,但它会显著减少重复,并为与PropertyGroup
的数据属性交互提供更大的灵活性。
要理解的一个重要区别是,每当属性更改时都会调用update
函数——它不是作为更新定义其属性的一种方式来调用的。相反,它提供了一种将特定属性的更改传达给程序的其他部分的方法;这必须小心使用以避免副作用,并且因为没有检查来避免无限递归。
另一件需要注意的是,使用set
和get
函数意味着任何default
值都必须通过显式set
调用(而不是 kwarg
)来设置,但这也提供了在必要时挂钩on_load
方法的机会。最后,prop_methods
函数必须在调用它的任何PropertyGroup
类之前定义。
prop_methods
函数的骨架如下所示:
def prop_methods(call, prop=None):
def getter(self):
# getter function must check if prop attr has a value yet
# if no value, will throw error, so must set default
# can hook on load here
# and either way, return self[prop] value
def setter(self, value):
self[prop] = value
def updater(self, context):
self.update(context)
methods = {
"GET": getter,
"SET": setter,
"UPDATE": updater
}
return methods[call]
完整的实现可以写成:
def prop_methods(call, prop=None):
def getter(self):
try:
value = self[prop]
except:
set_default = prop_methods("SET", prop)
set_default(self, self.defaults[prop])
if hasattr(self, "on_load"):
self.on_load()
value = self[prop]
finally:
return value
def setter(self, value):
self[prop] = value
def updater(self, context):
self.update(context)
methods = {
"GET": getter,
"SET": setter,
"UPDATE": updater,
}
return methods[call]
为此,任何具有调用prop_methods
属性的类都需要一个名为defaults
的字典对象和一个名为update
的方法(该函数提供,但不需要on_load
方法)。在PG_Standoff
类中,这些调用将用于附加Standoff.mesh()
的返回值,以及无论何时修改metric_diameter
或height
属性。PG_Standoff
课程的其余部分可以写成:
class PG_Standoff(PropertyGroup):
# ...
defaults = { "metric_diameter": 2.5, "height": 3 }
standoff = Standoff()
def on_load(self):
if self.height and self.metric_diameter:
self.__set_mesh()
def update(self, context):
self.__set_mesh()
def __set_mesh(self):
self.mesh = self.standoff.mesh(
self.height, self.metric_diameter)
仅留下import
语句和register
、unregister
函数。在 register 函数中,我们还将从 ointerProperty
的实例中指向PG_PropertyGroup
类,从 Blender 的Scene
类型的新属性中引用,这将使从插件的其余部分访问变得简单。这将产生一个完整的standoff_props.py
模块,如下面的要点所示:
from bpy.props import PointerProperty, FloatProperty
from bpy.types import Mesh, PropertyGroup, Scene
from bpy.utils import register_class, unregister_class
from .standoff_mesh import Standoff
def prop_methods(call, prop=None):
def getter(self):
try:
value = self[prop]
except:
set_default = prop_methods("SET", prop)
set_default(self, self.defaults[prop])
if hasattr(self, "on_load"):
self.on_load()
value = self[prop]
finally:
return value
def setter(self, value):
self[prop] = value
def updater(self, context):
self.update(context)
methods = {
"GET": getter,
"SET": setter,
"UPDATE": updater,
}
return methods[call]
class PG_Standoff(PropertyGroup):
metric_diameter: FloatProperty(
name="Inner Diameter (Metric)",
min=2,
max=5,
step=50,
precision=1,
set=prop_methods("SET", "metric_diameter"),
get=prop_methods("GET", "metric_diameter"),
update=prop_methods("UPDATE"))
height: FloatProperty(
name="Standoff Height",
min=2,
max=6,
step=25,
precision=2,
set=prop_methods("SET", "height"),
get=prop_methods("GET", "height"),
update=prop_methods("UPDATE"))
mesh: PointerProperty(type=Mesh)
defaults = { "metric_diameter": 2.5, "height": 3 }
standoff = Standoff()
def on_load(self):
if self.height and self.metric_diameter:
self.__set_mesh()
def update(self, context):
self.__set_mesh()
def __set_mesh(self):
self.mesh = self.standoff.mesh(self.height, self.metric_diameter)
def register():
register_class(PG_Standoff)
Scene.Standoff = PointerProperty(type=PG_Standoff)
def unregister():
unregister_class(PG_Standoff)
del Scene.Standoff
4、standoff_operator.py
最后两个模块都是短文件,实现起来非常简单,因为结构化和修改数据的繁重工作已经以简化其与 Blender Python API 交互的方式完成。在standoff_operator
模块中,我们将定义(并注册)一个新bpy.types.Operator
类,然后可以将其附加到任何 UI 按钮。
如何定义一个新的Operator
有一些要求,但这些都是有据可查且简单明了的,并且通过从 CLI 启动 Blender,警告和错误消息将立即显现错误配置的Operator
类有。手册部分给出了新Operators
的预期定义的详细信息。下面脚本将定义docstring
、bl_idname
、bl_label
和bl_options
属性的值。它还将定义一个execute
方法,该方法需要self
和context
参数,并包含将附加到任何 UI 按钮调用注册bl_idname
名下的Operator
逻辑和事件。
import bpy
from bpy.types import Operator
from bpy.utils import register_class, unregister_class
class DEMORACK_OT_AddNewStandoff(Operator):
"""adds standoff to test add-on registered ok"""
bl_idname = 'scene.add_new_standoff'
bl_label = 'New Standoff'
bl_options = { "REGISTER", "UNDO" }
def execute(self, context):
name = "Standoff"
standoff = context.scene.Standoff # <- set in standoff_props.register()
collection = context.scene.collection
obj = bpy.data.objects.new(name, standoff.mesh)
collection.objects.link(obj)
obj.select_set(True)
context.view_layer.objects.active = obj
return { "FINISHED" }
def register():
register_class(DEMORACK_OT_AddNewStandoff)
def unregister():
unregister_class(DEMORACK_OT_AddNewStandoff)
5、standoff_panel.py
定义一个新Panel
类几乎遵循与定义一个新Operator
类相同的模式,并且从概念上讲,只需将execute
最后一步中的draw
方法换成这一步中的方法。相关的Panel 手册部分提供了几个有用的示例, UI 脚本 > 模板 > Python 菜单中包含更多示例。 在手册bpy.types.UILayout
部分中可以发现了更多可能性,该部分记录了Panel
对象的导入项。在draw
方法中,它是一个简单(且非常开放)的过程:
- 访问
context
中的相关数据对象 - 为需要按钮的任何
Operator
调用创建layout.operator
对象 - 为用户可写数据属性创建
layout.prop
对象
当然,在该模式以及更复杂的数据类型中,还有很多扩展和变化的空间。但在基础上,这是另一个由 API 处理繁重工作的地方,并遵循bpy.props
和bpy.types.PropertyGroup
实例的内置使用模式。由于这种简单性,这是另一个模块,它足够简单:
from bpy.types import Panel
from bpy.utils import register_class, unregister_class
class DemoRackPanel:
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "DemoRack"
class StandoffPanel(DemoRackPanel, Panel):
bl_idname = "DEMORACK_PT_standoff_panel"
bl_label = "Standoff"
def draw(self, context):
layout = self.layout
standoff_data = context.scene.Standoff # <- set in standoff_props.register()
layout.operator("scene.add_new_standoff") # <- registered in standoff_operator.py
layout.prop(standoff_data, "metric_diameter")
layout.prop(standoff_data, "height")
def register():
register_class(StandoffPanel)
def unregister():
unregister_class(StandoffPanel)
唯一的额外变化是 DemoRackPanel
的类定义,它与 bpy.types.Panel
一起被StandoffPanel
继承。因为这不是DemoRack
插件中的唯一Panel
,它们都将位于 View 3D 侧抽屉中的单个选项卡下,消除 3 行重复是一件简单的事情。layout.prop
的模式和相关函数中是将 Data 对象作为第一个参数,并将该对象内属性的字符串标识符作为第二个参数。
剩下要做的就是从DemoRack/src/
目录编译DemoRack.zip
文件,然后像其他任何文件一样在编辑 > 首选项 > 附加组件中安装该本地文件。
原文链接:Build a Blender Add-on Ready to Scale
BimAnt翻译整理,转载请标明出处