成熟的 Blender 3D 插件是令人惊奇的事件。作为 Python 和 Blender 的老手,我常常发现自己被社区中的人们发明的弱小的货色弄得目瞪口呆。坦率地说,其中一些包看起来有点神奇,当自我狐疑或冒名顶替综合症的唠叨声音被突破时,很容易想到“如果有人能做出能够做xxx的货色就好了” 。
而后我记得,通过将好奇心和执著与良好的文档相结合,某人能够是任何人,X能够成为X、Y 和 Z。即便是艰难的局部也能够弄清楚——尤其是因为所有执著和好奇的人确保 Blender 的 Python文档和stackexchange与它让咱们创立的 gee whiz 图形一样好。
同样的形式,曾经存在的文档和模型为从头开始编写 Blender 插件奠定了平滑的根底,在开始时为该插件提供可扩大的构造有助于展现 Python API 的各个局部如何更多地组合在一起分明地。换句话说,它使新编写的代码比原来更好,同时也使现有代码更容易学习。
在本文的最初,咱们将创立一个功能齐全且已装置的插件,它提供了一个自定义 UI 元素来将Standoff 增加到 Blender 场景中,并带有界面控件来调整创立的网格的直径和高度。
一、文件构造
本文开端将存在的残缺目录和文件构造,咱们能够应用mkdir和touch形式进行创立,本示例是一个填空游戏。我正在调用我的项目DemoRack并将其设置为我用于 Python 我的项目的文件夹中的顶级目录名称:它不肯定必须是 Blender 特定的任何中央。上面是DemoRack我的项目的文件构造:
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 文档形容了这些函数所表演的角色:
二、__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 sysimport importlibmodule_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()
三、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, FloatPropertyfrom bpy.types import Mesh, PropertyGroup, Scenefrom bpy.utils import register_class, unregister_classfrom .standoff_mesh import Standoffdef 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
四、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 bpyfrom bpy.types import Operatorfrom bpy.utils import register_class, unregister_classclass 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)
五、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 Panelfrom bpy.utils import register_class, unregister_classclass 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