前言

  最近刚好有用 Blender 开发工具,于是研究了一下如何使用 Blender Python 来实现简单的工具。

Blender 印象

  虽然久仰 Blender 大名了,一直吐槽 Maya 的不思进取,将 Blender 奉为圭臬。但是其实我并没有使用过它。
  目前Blender 已经到 2.9 的版本,界面风格都好看了不少,整体透露着精致。
  以前老版本的界面和 Unity 的灰色界面一个调性,说实话我还是喜欢深色一点风格。
  而且 blender 整体的大小优化得很好,功能也很齐全,启动速度还快了很多。
  然而遗憾的是终究没有太多时间深入来学习它,毕竟对我目前而言,学习 Houdini 更为重要。

Blender 下载&界面

  Blender 下载可以去到 Blender 官网下载地址

alt

  点击大大的下载按钮下载安装即可。
  安装上其实无脑按确认和下一步即可。
  安装完成之后可以打开界面如下图所示。

alt

  默认会启动一个界面进行快速配置。

alt

  快捷键 shortcut 推荐使用 industry compatible
  毕竟我好歹也是 Maya 用户迁移过来的,Maya Houdini Unreal 主流的界面操作方式已经成为标准了。
  所以使用这个就不用去研究 Blender 自身的三维控制操作,让我舒服了很多。

  在上面的界面点击确认会在 %appdata%\Roaming\Blener Foudnation\Blender\${version}\config 这个路径下创建 userpref.blend 的文件。
  如果配置错了,可以把这个文件删除,重启 Blender 可以重新配置。

Blender 编程界面准备

alt

  新版本的 Blender 已经将不同的功能界面整合到最上面了。
  编程方面可以直接选择最右边的 Scripting

alt

  Blender 使用 Python3 ,可以在这个界面下新建一个文本编写代码。
  代码 print 的信息可以在 Windows > Toggle System Console 打开查看信息。 链接
  如果没有双屏,可以使用 PowerToys 快速分屏将 Blender 的窗口和 Console 摆放好,方便查看输出。

alt

  另外如果可以打印到 Python Console 也挺好的,所以我找到这个链接通过重载 print 方法可以实现。

bpy 编程

  blender 内置了 bpy 模块,从而可以通过 Python 代码来自动化 Blender 。
  学习编程还是离不开从文档开始 链接
  blender.stackexchange 是个非常好的提问网站,比起 Maya 的编程,这里有大量大佬提供了很多问题的解决方案。

  关于 Blender 的 GUI 编程可以参考官方文档 Panel 相关的介绍 链接
  Blender 并不是用 Qt 框架写的,所以界面上稍微没有那么灵活,不过也可以通过 Python 安装 PyQt 或者 PySide2 相关的 GUI 库来编写界面。

GUI 界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import bpy


class HelloWorldPanel(bpy.types.Panel):
bl_idname = "OBJECT_PT_hello_world"
bl_label = "Hello World"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"

def draw(self, context):
self.layout.label(text="Hello World")


bpy.utils.register_class(HelloWorldPanel)

alt

  官方案例运行起来如上图所示。
  注意: 官方要求 bl_idname 必须 CATEGORY_PT_name 一这样的形式命名,否则 Console 会弹出警告。

  更多 Blender 插件是直接配置 Category

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import bpy

class HelloWorldPanel(bpy.types.Panel):
bl_idname = "OBJECT_PT_hello_world2"
bl_label = "Hello World"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = "objectmode"
bl_category = "MyCategory"

def draw(self, context):
self.layout.label(text="Hello World")

bpy.utils.register_class(HelloWorldPanel)

alt

  blender 的 GUI 编程目前我接触得很不深,初步使用和 Mel GUI 编程很类似,通过 layout 布局进行配置 链接
  layout 通过 colum grid_flow 等不同的方法进行 GUI 排列。
  虽然没有 Qt 编程那么灵活,但是胜在简单明了。


  另外 Blender 还支持通过 property 配置生成 GUI 的操作, 类似 Maya 的这个 qargparse 库 链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import os
import bpy

from bpy.props import StringProperty, PointerProperty
from bpy.types import Panel, PropertyGroup

class MyProperties(PropertyGroup):
directory : StringProperty(
name="",
description="Path to Directory",
default="",
maxlen=1024,
subtype='DIR_PATH')

bpy.utils.register_class(MyProperties)


class ButtonOperator(bpy.types.Operator):
bl_idname = "mf_blender.select_path"
bl_label = "Print Directory"

def execute(self, context):
scn = context.scene
path = scn.my_tool.directory
print(path)
return {'FINISHED'}

bpy.utils.register_class(ButtonOperator)

class OBJECT_PT_DirectoryPanel(Panel):
bl_idname = "OBJECT_PT_Directory"
bl_label = "Directory Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "MyPanel"
bl_context = "objectmode"

def draw(self, context):
layout = self.layout
scn = context.scene
col = layout.column(align=True)
col.prop(scn.my_tool, "directory", text="directory")
col.operator("mf_blender.select_path")

bpy.utils.register_class(OBJECT_PT_DirectoryPanel)
bpy.types.Scene.my_tool = PointerProperty(type=MyProperties)

alt

  上面的脚本实现获取路径,点击按钮打印输出路径。
  Blender 会根据 StringProperty 的配置自动生成获取路径的 UI
  通过 Scene 绑定 property 来实现数据共享。


  当然上面的操作获取路径比较繁琐,能否只通过一个按钮来获取路径并且打印呢?
  当然也是可以的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import os
import bpy

from bpy.props import StringProperty, PointerProperty
from bpy.types import Panel, PropertyGroup

class ButtonOperator(bpy.types.Operator):
bl_idname = "mf_blender.select_path"
bl_label = "Import Directory"

directory : StringProperty(
name="",
description="Path to Directory",
default="",
maxlen=1024,
subtype='DIR_PATH')

# NOTE invoke 是作为 GUI 调用执行的函数
def invoke(self, context, event):
# NOTE 注意这个函数放到 execute 函数执行会导致 blender 崩溃
# NOTE 打开文件选择窗口
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}

# NOTE execute 默认调用执行的函数
def execute(self, context):
print(self.directory)
return {'FINISHED'}

bpy.utils.register_class(ButtonOperator)

class OBJECT_PT_DirectoryPanel(Panel):
bl_idname = "OBJECT_PT_Directory"
bl_label = "Directory Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "MyPanel"
bl_context = "objectmode"

def draw(self, context):
layout = self.layout
scn = context.scene
col = layout.column(align=True)
# col.prop(scn.my_tool, "directory", text="directory")
col.operator("mf_blender.select_path")

bpy.utils.register_class(OBJECT_PT_DirectoryPanel)

材质节点自动化

1
2
3
4
5
6
7
8
9
10
11
12
import bpy
# NOTE 获取默认场景的 Cube object
obj = bpy.data.objects["Cube"]
# 注: 获取当前选择的物体可以用 bpy.context.selected_objects

# NOTE 获取默认的 Material 材质插槽
slot = obj.material_slots["Material"]
print(slot) # 打印 <bpy_struct, MaterialSlot("Material") at 0x0000028D6166AB88>
mat_name = slot.name
print(mat_name) # 打印 Material
material = slot.material
print(material) # 打印 <bpy_struct, Material("Material") at 0x0000028D6347EEC8>

  上面的方法可以获取到特定的材质
  objects material_slots 这些方法在 Blender 里面既可以用数组的序号获取也可以用字典的名称来获取。

1
2
3
4
5
6
7
8
9
10
# NOTE 进入材质节点
matnodes = material.node_tree.nodes
# NOTE 生成贴图节点
texture=matnodes.new("ShaderNodeTexImage")
# NOTE 加载一张贴图
image_path = r"C:\Users\timmyliang\Pictures\Sketchpad.png"
texture.image = bpy.data.images.load(image_path)
# NOTE 连接两个节点的通道
channel = matnodes['Principled BSDF'].inputs["Base Color"]
material.node_tree.links.new(channel, texture.outputs["Color"])

alt

  上面执行可以实现贴图节点连接到材质的颜色通道上。

开启加载脚本 - 配置顶部菜单项

https://blender.stackexchange.com/questions/2738

  根据上面的回答,可以配置用户的 Scripts 目录路径,然后再路径下添加 startup 文件夹实现开启 Blender 自动执行。

https://devtalk.blender.org/t/blender-user-scripts-and-multiple-paths/10395

  修改用户的 Blender 配置比较麻烦,可以用上面链接提到的环境变量来配置 Blender 的启动路径

$BLENDER_USER_CONFIG Directory for user configuration files.
$BLENDER_USER_SCRIPTS Directory for user scripts.
$BLENDER_SYSTEM_SCRIPTS Directory for system wide scripts.

  有了这些之后就可以实现类似 Maya userSetup.py 脚本的效果。


  下面我们想要实现 Blender 添加顶部自定义菜单,点击菜单弹出提示弹窗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# NOTE https://www.youtube.com/watch?v=3C6wVPVrPtM
import bpy
from bpy.utils import register_class


class WM_OT_myOp(bpy.types.Operator):
bl_label = "Alert Dialog"
bl_idname = "wm.myop"

# https://blender.stackexchange.com/a/23976
bl_options = {'REGISTER', 'UNDO'}

def execute(self,context):
return {'FINISHED'}

def draw(self,context):
self.layout.label(text="Custom Dialog Message")

def invoke(self,context,event):
# NOTE https://blender.stackexchange.com/a/14319
return context.window_manager.invoke_props_dialog(self)
# NOTE 使用 invoke_props_popup 可以让弹窗没有按钮
# return context.window_manager.invoke_props_popup(self,event)

register_class(WM_OT_myOp)
bpy.ops.wm.myop('INVOKE_DEFAULT')

  上面的脚本可以实现自定义弹窗。

alt

  更简单的弹窗配置方案 链接

1
2
3
4
5
import bpy
def show_message(message="", title="Message Box", icon="INFO"):
bpy.context.window_manager.popup_menu(
lambda self, _: self.layout.label(text=message), title=title, icon=icon
)

  下面就是想办法配置顶部的菜单项然后触发上面的 Operator 弹窗

https://blender.stackexchange.com/questions/156652

  根据上面的链接可以知道,自定义菜单栏添加可以使用 TOPBAR_MT_editor_menus.append

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import bpy
from bpy.utils import register_class

class WM_OT_MessageDialog(bpy.types.Operator):
bl_label = "Alert Dialog"
bl_idname = "wm.message_dialog"

def execute(self, context):
return {"FINISHED"}

def draw(self, context):
self.layout.label(text="Custom Dialog Message")

def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)

class TOPBAR_MT_custom_menu(bpy.types.Menu):
bl_label = "Custom Menu"
# NOTE 没有设置 bl_idname 自动拿 class name 作为 id

def draw(self, context):
layout = self.layout
layout.operator(WM_OT_MessageDialog.bl_idname, text="Open Message Dialog")

@classmethod
def menu_draw(cls, self, context):
self.layout.menu(cls.__name__)


register_class(WM_OT_MessageDialog)
register_class(TOPBAR_MT_custom_menu)
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR_MT_custom_menu.menu_draw)

alt

  bl_idname 没有设置自动用 class 的名称作为 id 链接

总结

  Blender 工具开发接触还比较浅,目前体验的感觉是灵活度欠缺了很多,用惯了 Qt 框架,面对新的框架编程充满了不习惯。
  可能还是因为没有找到好用的 ui 可视化界面工具吧,比较怀念 QtDesigner 的开发体验。 (接触尚浅,太菜了,有好的经验欢迎大家分享。)
  不过 Blender 虽然没那么灵活,也实实在在的简单高效~