插件教程

目标读者

本篇教程旨在帮助技术型艺术家或者开发者学习如何扩展 Blender。对于通读全文的读者,需要通晓 Python 的基础知识。

前提

阅读本教程前,用户需要...

  • 熟悉 Blender 中的基础功能。

  • 知道如何在 Blender 的文本编辑器中运行脚本。

  • 了解 Python 基元类型(整数、布尔值、字符串、列表、元组、字典和集合)。

  • 熟悉 Python 模块的概念。

  • 对 Python 中的类(面向对象)有基础认知。

学习本教程前建议阅读。

为了最好地解决 Python 在编写脚本时打印的任何错误消息,请从终端运行 Blender。请参阅使用终端

Tip

You can enable Developer Extras in the preferences to enable features that make developing add-ons easier.

何为插件?

插件是附加一些额外要求的 Python 模块,这样 Blender 可以列表形式显示有用信息。

下面的例子是一个最简单的插件:

bl_info = {
    "name": "My Test Add-on",
    "blender": (2, 80, 0),
    "category": "Object",
}
def register():
    print("Hello World")
def unregister():
    print("Goodbye World")
bl_info

一个字典,包含插件元数据如标题、版本和作者,这些信息会显示在用户设置的插件列表。它还指定了运行该脚本的最低版本要求;更老的版本无法在插件列表中显示该插件。

register

仅在启用插件时运行的函数,这意味着无需激活插件即可加载模块。

unregister

用于卸载 register 建立的数据的函数,在禁用插件时调用。

注意:该插件不会进行任何 Blender 相关操作(比如不会载入 blender_api:bpy 模块)。

这是一个刻意设计的插件示例,用于说明插件的基础要求其实很简单。

插件通常会注册操作、面板、菜单选项等,不过这没有多大价值,从文本编辑器,甚至交互控制台执行的脚本也可以做到 -- 插件与 Blender 的结合方式并没有本质上的不同,这些功能都是 blender_api:bpy 模块提供的,任何脚本都可以访问。

所以,插件仅仅是一种封装 Python 模块的方法,以方便用户使用。

Note

在文本编辑器运行这个脚本不会有任何输出,必须通过用户设置安装该脚本才能看到输出。启用和禁用时均会打印提示信息。

第一个插件

上文中的最简插件除举例外别无它用。下面的插件也很简单,不过演示了如何使用一个 Operator 将脚本集成到Blender,Operator 是用于定义可从菜单、按钮和快捷键访问的工具的特有方法。

首先,写一个移动场景内所有物体的脚本。

写脚本

在文本编辑器添加下面的脚本:

import bpy

scene = bpy.context.scene
for obj in scene.objects:
    obj.location.x += 1.0

按下运行脚本按钮,活动场景内的所有物体均会移动 1 个 Blender 单位。

写插件 (简易)

这个插件使用了上面的脚本,并将其添加到一个 operator 的 execute() 函数中:

bl_info = {
    "name": "Move X Axis",
    "blender": (2, 80, 0),
    "category": "Object",
}

import bpy


class ObjectMoveX(bpy.types.Operator):
    """My Object Moving Script"""      # Use this as a tooltip for menu items and buttons.
    bl_idname = "object.move_x"        # Unique identifier for buttons and menu items to reference.
    bl_label = "Move X by One"         # Display name in the interface.
    bl_options = {'REGISTER', 'UNDO'}  # Enable undo for the operator.

    def execute(self, context):        # execute() is called when running the operator.

        # The original script
        scene = context.scene
        for obj in scene.objects:
            obj.location.x += 1.0

        return {'FINISHED'}            # Lets Blender know the operator finished successfully.

def menu_func(self, context):
    self.layout.operator(ObjectMoveX.bl_idname)

def register():
    bpy.utils.register_class(ObjectMoveX)
    bpy.types.VIEW3D_MT_object.append(menu_func)  # Adds the new operator to an existing menu.

def unregister():
    bpy.utils.unregister_class(ObjectMoveX)


# This allows you to run the script directly from Blender's Text editor
# to test the add-on without having to install it.
if __name__ == "__main__":
    register()

Note

bl_info 被分割成多行,这仅仅是一种编程风格,可以方便添加元素。

Note

这里传递给 execute() 的参数是 context.scene,而非 bpy.context.scene。大多数情况下,两者是一致的。然而在一些情况下,传递给 operators 的是自定义 context,所以脚本作者更愿意传递 context 参数至 operator。

要测试这段脚本,你可以将其复制粘贴到 Blender 文本编辑器,并运行。这将直接执行脚本,并立即调用 register 函数。

不过,运行该脚本不能直接移动物体。你需要执行新注册的 operator 。

../../_images/advanced_scripting_addon-tutorial_operator-search-menu.png

运算符搜索菜单。

打开运算符搜索菜单并键入 "Move X by One"( bl_label ),然后键入 Return

物体将与前文一样移动。

不要关闭文本编辑器,下一步安装需要用到

安装插件

在 Blender 文本编辑器写好插件后,你可能想要安装该插件,这样就可以在用户设置中启用启动时加载插件。

尽管上面的插件只是一个测试,但还是把这些步骤都走一遍,这样以后就知道该怎么做了。

要安装 Blender 文本编辑器里写的插件,首先需要存盘,注意遵守 Python 模块的命名限制,并以 .py 作为扩展名。

存盘之后,就可以跟从网上下载的插件一样安装了。

打开 偏好设置 ‣ 插件 ‣ 安装...,并选择该文件。

现在将列出该插件,您可以通过按复选框来启用它,如果您希望在重新启动时启用它,请按 另存为默认值。该运算符的运行方式与上一节中所述的方式相同。

启用插件后,Blender 将执行代码并运行 register() 函数。禁用插件后,Blender 则运行 unregister() 函数。

Note

插件的安装位置取决于你的 Blender 配置。安装插件时,控制台会提示插件的原始和目标路径。在 Python 控制台运行下面的脚本,也可以查找插件目标路径:

import addon_utils
print(addon_utils.paths())

更多这方面内容见:目录布局

第二个插件

第二个插件是关于物体实例化 -- 亦即 -- 以与阵列修改器类似的方法生成物体的关联副本。

写脚本

和前面一样,我们从一段脚本开始,然后将其转化为插件:

import bpy
from bpy import context

# Get the current scene
scene = context.scene

# Get the 3D cursor location
cursor = scene.cursor.location

# Get the active object (assume we have one)
obj = context.active_object

# Now make a copy of the object
obj_new = obj.copy()

# The new object has to be added to a collection in the scene
scene.collection.objects.link(obj_new)

# Now we can place the object
obj_new.location = cursor

现在,把这段脚本复制到 Blender 文本编辑器,选择立方体并运行脚本。运行脚本前记得单击将 3D 游标移动一旁,副本物体将出现在游标位置。

运行过后,进入 编辑模式 修改立方体后 -- 所有的副本会同时变化,这在 Blender 里称作 关联副本

下一步,把这段脚本加到循环里,在活动物体与游标之间生成物体阵列:

import bpy
from bpy import context

scene = context.scene
cursor = scene.cursor.location
obj = context.active_object

# Use a fixed value for now, eventually make this user adjustable
total = 10

# Add 'total' objects into the scene
for i in range(total):
    obj_new = obj.copy()
    scene.collection.objects.link(obj_new)

    # Now place the object in between the cursor
    # and the active object based on 'i'
    factor = i / total
    obj_new.location = (obj.location * factor) + (cursor * (1.0 - factor))

将 3D 游标远离活动物体,运行脚本,查看结果。

在这段脚本里,对物体和游标位置做了一点数学运算,这是因为两者都是 3D mathutils.Vector 实例,mathutils 模块提供了这个方便的类,并且允许向量与数值和矩阵做乘法运算。

如果你对这方面感兴趣的话,可以进一步阅读 mathutils.Vector -- 这里有很多方便的工具函数如计算向量夹角、叉乘、点乘,mathutils.geometry 还提供了更高级的函数如 Bézier 样条曲线插值和射线-三角形相交。

现在我们专心将这段脚本编程插件,不过知道这个 3D 数学模块也是不错的,以后更高级的功能可能用到它。

写插件

首先将脚本转换为插件:

bl_info = {
    "name": "Cursor Array",
    "blender": (2, 80, 0),
    "category": "Object",
}

import bpy


class ObjectCursorArray(bpy.types.Operator):
    """Object Cursor Array"""
    bl_idname = "object.cursor_array"
    bl_label = "Cursor Array"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        cursor = scene.cursor.location
        obj = context.active_object

        total = 10

        for i in range(total):
            obj_new = obj.copy()
            scene.collection.objects.link(obj_new)

            factor = i / total
            obj_new.location = (obj.location * factor) + (cursor * (1.0 - factor))

        return {'FINISHED'}

def register():
    bpy.utils.register_class(ObjectCursorArray)


def unregister():
    bpy.utils.unregister_class(ObjectCursorArray)


if __name__ == "__main__":
    register()

接下来的步骤上文已经介绍过了,不过你还是可以运行脚本,再考虑一下如何改进这插件的功能。

The two of the most obvious missing things are -- having the total fixed at 10, and having to access the operator with 搜索 is not very convenient.

下文将讲解如何改进,并给出最终脚本代码。

操作属性

有多种属性类型可用于工具设置,常见的属性类型包括:整型、浮点型、矢量型、颜色、布尔型和字符串型。

这些属性与 Python 类的属性用法不同,因为 Blender 需要在界面显示、保存其键位映射设置,并保留设置供下次使用。

尽管这是相当 Python 化的方式,但是记住,实际上你定义的工具设置会被加载到 Blender,并被 Blender 的其他部分从 Python 外部访问。

要消除 总数 为 10 的问题,会用到一个操作属性。操作属性是通过 bpy.props 模块定义的,将下面的代码加到 operator 类的主体中:

# moved assignment from execute() to the body of the class...
total: bpy.props.IntProperty(name="Steps", default=2, min=1, max=100)

# and this is accessed on the class
# instance within the execute() function as...
self.total

这些来自 bpy.props 的属性会被 Blender 特别处理,当(operator)类被注册后,会在界面显示属性按钮。可以传递很多参数给属性,如设置上下界、修改默认值和显示工具提示。

这篇文档不会详细介绍如何使用其他属性类型,不过上面的链接包含了更高级的属性用法范例。

键位映射

在 Blender 中,插件可以有自己的键位映射,以避免与 Blender 内置键位映射冲突。

在下面的例子中,添加了一个新的物体模式 bpy.types.KeyMap,接着向该键位映射添加了一个 bpy.types.KeyMapItem,指向新添加的操作,使用 Shift-Ctrl-T 作为快捷键:

# store keymaps here to access after registration
addon_keymaps = []

def register():

    # handle the keymap
    wm = bpy.context.window_manager
    km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY')

    kmi = km.keymap_items.new(ObjectCursorArray.bl_idname, 'T', 'PRESS', ctrl=True, shift=True)
    kmi.properties.total = 4

    addon_keymaps.append((km, kmi))


def unregister():

    # handle the keymap
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()

值得注意的是,键位映射的 total 值与 operator 的默认值不同,这样就可以通过不同按键组合访问不同设置的同一 operator 。

Note

While Shift-Ctrl-T is not a default Blender key shortcut, it is hard to make sure add-ons will not overwrite each other's keymaps. Thus at least take care when assigning keys that they do not conflict with important functionality of Blender (see also Is Key Free).

上文所用函数的 API 文档,见:

合二为一

bl_info = {
    "name": "Cursor Array",
    "blender": (2, 80, 0),
    "category": "Object",
}

import bpy


class ObjectCursorArray(bpy.types.Operator):
    """Object Cursor Array"""
    bl_idname = "object.cursor_array"
    bl_label = "Cursor Array"
    bl_options = {'REGISTER', 'UNDO'}

    total: bpy.props.IntProperty(name="Steps", default=2, min=1, max=100)

    def execute(self, context):
        scene = context.scene
        cursor = scene.cursor.location
        obj = context.active_object

        for i in range(self.total):
            obj_new = obj.copy()
            scene.collection.objects.link(obj_new)

            factor = i / self.total
            obj_new.location = (obj.location * factor) + (cursor * (1.0 - factor))

        return {'FINISHED'}


def menu_func(self, context):
    self.layout.operator(ObjectCursorArray.bl_idname)

# store keymaps here to access after registration
addon_keymaps = []


def register():
    bpy.utils.register_class(ObjectCursorArray)
    bpy.types.VIEW3D_MT_object.append(menu_func)

    # handle the keymap
    wm = bpy.context.window_manager
    # Note that in background mode (no GUI available), keyconfigs are not available either,
    # so we have to check this to avoid nasty errors in background case.
    kc = wm.keyconfigs.addon
    if kc:
        km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY')
        kmi = km.keymap_items.new(ObjectCursorArray.bl_idname, 'T', 'PRESS', ctrl=True, shift=True)
        kmi.properties.total = 4
        addon_keymaps.append((km, kmi))

def unregister():
    # Note: when unregistering, it's usually good practice to do it in reverse order you registered.
    # Can avoid strange issues like keymap still referring to operators already unregistered...
    # handle the keymap
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()

    bpy.utils.unregister_class(ObjectCursorArray)
    bpy.types.VIEW3D_MT_object.remove(menu_func)


if __name__ == "__main__":
    register()
../../_images/advanced_scripting_addon-tutorial_in-menu.png

菜单选项。

运行该脚本(或保存后通过用户设置安装),操作会出现在 物体 菜单中。

../../_images/advanced_scripting_addon-tutorial_op-prop.png

操作属性。

从菜单执行操作后,你还可以选择添加的立方体数量。

Note

多次直接执行脚本也会每次添加菜单。虽然没有有用的行为,但没有什幺可担心的,因为当通过首选项启用时,加载项不会多次注册自己。

结语

插件可以整洁地封装特定功能,用于编写工具改善工作流,或者编写功能供其他人使用。

尽管 Blender 中 Python 可以做的还存在一些限制,不过已经可以实现相当多的功能,使用户免于钻研 Blender 的 C/C++ 源码。

教程里可以给出的范例始终是有限的,不过已经演示了用于常见任务的 Blender API,用户可以借此延伸出自己的工具。

扩展阅读

Blender 附带了一些注释过的模板,可以在文本编辑器标题栏找到,如果你需要某个方面的示例代码,从这里开始是一个不错的选择。

看完上面的教程过后,这里提供了几个你可能用到的网址。