插件教程

目标读者

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

前提

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

  • 熟悉Blender中的基础功能。
  • 知道如何在Blender的文本编辑器中运行脚本。
  • 理解Python的基础基础类型(整型、布尔、字符串、列表、元组、字典和集合)。
  • 熟悉Python模块的概念。
  • 对Python中的类(面向对象)有基础认知。学习本教程前建议阅读。

  • 深入Python 章节(1, 2, 3, 4, 和 7)。

  • Blender API快速入门 有助于更加熟悉Blender/Python基础。为了在编写脚本时更好的排查Python打印的错误,可以从终端启动Blender。参见 使用终端

文档链接

在阅读教程时,用户可能需要查找参考文档。

  • blender_api:Blender API概述. - 这份文档相当详细,不过如果你想对某个话题了解更多的话,这很有用。
  • bpy.context API参考. - 可以方便地获得脚本可以调用的选项。
  • bpy.types.Operator. - 下文的插件会定义操作,这些文档提供了更多操作的范例和细节。

何为插件?

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

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

  1. bl_info = {"name": "My Test Add-on", "category": "Object"}
  2. def register():
  3. print("Hello World")
  4. def unregister():
  5. print("Goodbye World")
  • bl_info
  • 一个字典,包含插件元数据如标题、版本和作者,这些信息会显示在用户设置的插件列表。
  • register
  • 仅在启用插件时运行的函数,这意味着无需激活插件即可加载模块。
  • unregister
  • 用于卸载 register 建立的数据的函数,在禁用插件时调用。注意:该插件不会进行任何Blender相关操作(比如不会载入 bpy 模块)。

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

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

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

Note

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

第一个插件

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

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

写脚本

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

  1. import bpy
  2.  
  3. scene = bpy.context.scene
  4. for obj in scene.objects:
  5. obj.location.x += 1.0

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

写插件 (简易)

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

  1. bl_info = {
  2. "name": "Move X Axis",
  3. "category": "Object",
  4. }
  5.  
  6. import bpy
  7.  
  8.  
  9. class ObjectMoveX(bpy.types.Operator):
  10. """My Object Moving Script""" # Use this as a tooltip for menu items and buttons.
  11. bl_idname = "object.move_x" # Unique identifier for buttons and menu items to reference.
  12. bl_label = "Move X by One" # Display name in the interface.
  13. bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator.
  14.  
  15. def execute(self, context): # execute() is called when running the operator.
  16.  
  17. # The original script
  18. scene = context.scene
  19. for obj in scene.objects:
  20. obj.location.x += 1.0
  21.  
  22. return {'FINISHED'} # Lets Blender know the operator finished successfully.
  23.  
  24. def register():
  25. bpy.utils.register_class(ObjectMoveX)
  26.  
  27.  
  28. def unregister():
  29. bpy.utils.unregister_class(ObjectMoveX)
  30.  
  31.  
  32. # This allows you to run the script directly from Blender's Text editor
  33. # to test the add-on without having to install it.
  34. if __name__ == "__main__":
  35. 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操作搜索菜单。

按下 Spacebar 弹出操作搜索菜单,输入 "Move X by One" (bl_label), 然后按下 Return 执行操作。

物体将与前文一样移动。

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

安装插件

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

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

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

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

打开 文件 ‣ 用户设置 , 进入 插件 选项卡,按下 安装插件… 按钮,并选择该文件。

现在插件就添加到了插件列表,可以勾选启用,如果要在下次启动时加载,可以 保存用户设置

Note

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

  1. import addon_utils
  2. print(addon_utils.paths())

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

第二个插件

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

写脚本

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

  1. import bpy
  2. from bpy import context
  3.  
  4. # Get the current scene
  5. scene = context.scene
  6.  
  7. # Get the 3D cursor
  8. cursor = scene.cursor_location
  9.  
  10. # Get the active object (assume we have one)
  11. obj = scene.objects.active
  12.  
  13. # Now make a copy of the object
  14. obj_new = obj.copy()
  15.  
  16. # The object won't automatically get into a new scene
  17. scene.objects.link(obj_new)
  18.  
  19. # Now we can place the object
  20. obj_new.location = cursor

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

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

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

  1. import bpy
  2. from bpy import context
  3.  
  4. scene = context.scene
  5. cursor = scene.cursor_location
  6. obj = scene.objects.active
  7.  
  8. # Use a fixed value for now, eventually make this user adjustable
  9. total = 10
  10.  
  11. # Add 'total' objects into the scene
  12. for i in range(total):
  13. obj_new = obj.copy()
  14. scene.objects.link(obj_new)
  15.  
  16. # Now place the object in between the cursor
  17. # and the active object based on 'i'
  18. factor = i / total
  19. obj_new.location = (obj.location * factor) + (cursor * (1.0 - factor))

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

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

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

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

写插件

首先将脚本转换为插件:

  1. bl_info = {
  2. "name": "Cursor Array",
  3. "category": "Object",
  4. }
  5.  
  6. import bpy
  7.  
  8.  
  9. class ObjectCursorArray(bpy.types.Operator):
  10. """Object Cursor Array"""
  11. bl_idname = "object.cursor_array"
  12. bl_label = "Cursor Array"
  13. bl_options = {'REGISTER', 'UNDO'}
  14.  
  15. def execute(self, context):
  16. scene = context.scene
  17. cursor = scene.cursor_location
  18. obj = scene.objects.active
  19.  
  20. total = 10
  21.  
  22. for i in range(total):
  23. obj_new = obj.copy()
  24. scene.objects.link(obj_new)
  25.  
  26. factor = i / total
  27. obj_new.location = (obj.location * factor) + (cursor * (1.0 - factor))
  28.  
  29. return {'FINISHED'}
  30.  
  31. def register():
  32. bpy.utils.register_class(ObjectCursorArray)
  33.  
  34.  
  35. def unregister():
  36. bpy.utils.unregister_class(ObjectCursorArray)
  37.  
  38.  
  39. if __name__ == "__main__":
  40. register()

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

这个插件明显缺少两项功能 — 总数固定为10,从空格键访问该操作也不是很方便。

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

操作属性

用于工具设置的属性类型多种多样,常见的有: int, float, vector, color, boolean and string。

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

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

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

  1. # moved assignment from execute() to the body of the class...
  2. total = bpy.props.IntProperty(name="Steps", default=2, min=1, max=100)
  3.  
  4. # and this is accessed on the class
  5. # instance within the execute() function as...
  6. self.total

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

See also

bpy.props.IntProperty

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

菜单选项

插件可以添加到现有的面板、标题栏和菜单等Python定义的用户界面中。

这个例子里,我们将其添加到现有菜单。

../../_images/advanced_scripting_addon-tutorial_menu-id.png菜单标识符

鼠标在菜单上方停留,会出现提示,这就是菜单标识符。

添加菜单选项的方式是,在给现有类追加一个绘制函数:

  1. def menu_func(self, context):
  2. self.layout.operator(ObjectCursorArray.bl_idname)
  3.  
  4. def register():
  5. bpy.types.VIEW3D_MT_object.append(menu_func)

扩展菜单的文档,请参考 Menu(bpy_struct)

键位映射

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

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

  1. # store keymaps here to access after registration
  2. addon_keymaps = []
  3.  
  4. def register():
  5.  
  6. # handle the keymap
  7. wm = bpy.context.window_manager
  8. km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY')
  9.  
  10. kmi = km.keymap_items.new(ObjectCursorArray.bl_idname, 'SPACE', 'PRESS', ctrl=True, shift=True)
  11. kmi.properties.total = 4
  12.  
  13. addon_keymaps.append((km, kmi))
  14.  
  15.  
  16. def unregister():
  17.  
  18. # handle the keymap
  19. for km, kmi in addon_keymaps:
  20. km.keymap_items.remove(kmi)
  21. addon_keymaps.clear()

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

Note

尽管 Shift-Ctrl-Spacebar 不是Blender的默认快捷键,但也很难确保插件之间不会互相覆盖键位映射。至少,在指定快捷键时要注意避免与Blender重要功能出现冲突。

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

合二为一

  1. bl_info = {
  2. "name": "Cursor Array",
  3. "category": "Object",
  4. }
  5.  
  6. import bpy
  7.  
  8.  
  9. class ObjectCursorArray(bpy.types.Operator):
  10. """Object Cursor Array"""
  11. bl_idname = "object.cursor_array"
  12. bl_label = "Cursor Array"
  13. bl_options = {'REGISTER', 'UNDO'}
  14.  
  15. total = bpy.props.IntProperty(name="Steps", default=2, min=1, max=100)
  16.  
  17. def execute(self, context):
  18. scene = context.scene
  19. cursor = scene.cursor_location
  20. obj = scene.objects.active
  21.  
  22. for i in range(self.total):
  23. obj_new = obj.copy()
  24. scene.objects.link(obj_new)
  25.  
  26. factor = i / self.total
  27. obj_new.location = (obj.location * factor) + (cursor * (1.0 - factor))
  28.  
  29. return {'FINISHED'}
  30.  
  31.  
  32. def menu_func(self, context):
  33. self.layout.operator(ObjectCursorArray.bl_idname)
  34.  
  35. # store keymaps here to access after registration
  36. addon_keymaps = []
  37.  
  38.  
  39. def register():
  40. bpy.utils.register_class(ObjectCursorArray)
  41. bpy.types.VIEW3D_MT_object.append(menu_func)
  42.  
  43. # handle the keymap
  44. wm = bpy.context.window_manager
  45. # Note that in background mode (no GUI available), keyconfigs are not available either,
  46. # so we have to check this to avoid nasty errors in background case.
  47. kc = wm.keyconfigs.addon
  48. if kc:
  49. km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY')
  50. kmi = km.keymap_items.new(ObjectCursorArray.bl_idname, 'SPACE', 'PRESS', ctrl=True, shift=True)
  51. kmi.properties.total = 4
  52. addon_keymaps.append((km, kmi))
  53.  
  54. def unregister():
  55. # Note: when unregistering, it's usually good practice to do it in reverse order you registered.
  56. # Can avoid strange issues like keymap still referring to operators already unregistered...
  57. # handle the keymap
  58. for km, kmi in addon_keymaps:
  59. km.keymap_items.remove(kmi)
  60. addon_keymaps.clear()
  61.  
  62. bpy.utils.unregister_class(ObjectCursorArray)
  63. bpy.types.VIEW3D_MT_object.remove(menu_func)
  64.  
  65.  
  66. if __name__ == "__main__":
  67. 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附带了一些注释过的模板,可以在文本编辑器标题栏找到,如果你需要某个方面的示例代码,从这里开始是一个不错的选择。

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