王子正

Technical Artist

Blog Post

Blender插件制作心得

April 8, 2023 技术实现
Blender插件制作心得

前言

一个偶然的机会接触到 Blender插件编写,一直以来我都是(曾经是) Maya以及Houdini软件的拥护者,但是Blender 的开源社区逐渐壮大也不能不引起重视,那么是时候了解一下Blender的相关内容。

Blender的交互层都是用Python来编写的,这就相当友好了。作为一个使用过python用来数据分析以及编写爬虫爬 *** 内容的人表示这很开心。

基础模板

bl_info = {
    "name": "HelloAddon",
    "author": "作者",
    "version": (1, 0),
    "blender": (2, 79, 0),
    "location": "View3D > Tool Shelf >HelloAddon Panel",
    "description": "插件描述",
    "warning": "",
    "wiki_url": "",
    "category": "3D View",
}

import bpy
import logging
from . import model_data
from . import command_operators
from . import view_panels


def register():
    bpy.utils.register_module(__name__)
    command_operators.register()
    view_panels.register()


def unregister():
    bpy.utils.unregister_module(__name__)
    command_operators.unregister()
    view_panels.unregister()

if __name__ == "__main__":
    register()

整体思路还是比价简单的。以上代码就是套路,不需要理解,主体为bpy.utils.register_module(name),作用是注册import进来的所有模块。至于command_operators.register() 与 view_panels.register()则代表其他非模块相关的注册。这里的代码以后要新建工程直接ctrl c即可。直接运行main函数,就可以注册插件。z注册插件后就可以编写自己的插件内容并且分别在 register() 和 unregister() 中调用。

编写一个 check object命名的功能吧

我希望我的这个插件可以实现的功能是遍历当前场景内全部的object,然后根据命名规则判断正确还是错误,命名规则如下。

​ <asset_type><descriptive_name><descriptive_name>_<##>

  1. 其中<asset_type> 可以为 char, prop, veg, veh, arch 和 tree
  2. 中间的<descriptive_name>可以有2-3个不同的名字,并且全部为小写的英文字母
  3. 最后的<##> 是数量,从01 最大可以到99

并且把最终的结果输出到一个html网页中并渲染出来。最终效果如下

image-20230405185735496

先写基础模板把我的CheckNamesOperator 注册上去

import bpy

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


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


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.check_naming()

功能实现部分的思路如下。先遍历场景内的object, 然后读到这个object的当前命名内容,把他当作一串string。整体思路有点类似之前编程课写的string parser。这里把命名string用split("_")拆分为三部分并且放在一个tags list中

Prefix

第一部分的asset_type 是prefix 用tags[0] 提取,判断第一部分的类型,可以预先用一个list把valid prefixes提前写好,判定提取的string是否在list中

# Prefix
    if tags[0] in valid_prefixes:
        prefix_ok = True

Suffix

第三部分的## 用tags[-1]提取, 最后一部分的内容是长度为2 的数字。数字的判定用python自带的ValueError来判定,如果不报错就是数字。

# Suffix
nan = False
padding = len(tags[-1]) == suffix_padding

try:
    num = int(tags[-1])
except ValueError:
    nan = True

Identifiers

中间的部分descriptive_name用tags[1:-1] 提取,先判断中间部分有几个descriptive_name, 规则只允许2-3个。其次要求descriptive_name全部为小写的英文字母

# Identifiers
errors = 0
if len(tags) - 2 not in num_indentifiers_required:
    errors += 1
for id in tags[1:-1]:
    if not id.isalpha():
        errors += 1
    if not id == id.lower():
        errors += 1
if errors == 0:
    ids_ok = True

这样最核心的判定就完全结束了。现在来编写输出的部分

class CheckNamesOperator(bpy.types.Operator): “““Check Object Names””” bl_idname = “object.check_naming” bl_label = “Check Object Names”

@classmethod
def poll(cls, context):
    return context.active_object is not None

def execute(self, context):
    scene_objects = [[obj[0], obj[1]] for obj in bpy.data.objects.items()]

    results = []
    errors = 0

    for obj in scene_objects:
        v = validate_name(obj[0])
        if not all(v):
            errors += 1
        results.append({
            'object': obj[0],
            'prefix': 'OK' if v[0] else 'FAIL',
            'ids': 'OK' if v[1] else 'FAIL',
            'suffix': 'OK' if v[2] else 'FAIL',
            'pass': all(v)
        })

    if errors:
        self.message_errors(results)
    else:
        self.message_ok(message='No problems found!')

    return {'FINISHED'}

def message_ok(self, message="", title="OK!", icon='INFO'):
    def draw(self, context):
        self.layout.label(text=message)

    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)

def message_errors(self, results, title="Errors Found!", icon='ERROR'):
    scene_file = bpy.path.basename(bpy.data.filepath)
    scene_folder = path.dirname(bpy.data.filepath)

    base_file_name = '.'.join(scene_file.split('.')[0:-1]) + '.html'

    out_html_file = path.join(scene_folder, base_file_name)

    with open(out_html_file, 'w') as f:
        f.write('<html><head>')

        # Styles
        f.write("""
        <style type="text/css">
            table {
              margin: 0 auto;
              text-align: left;
            }
            tr.error{
              background-color: #990d0d;
              color: #ffd5d5;
            }
            tr.ok {
              background-color: green;
              color: #1a2615;
            }
            table {
              margin: 10% auto;
              width: 40%;
              background-color: dimgray;
            }
            th {
              color: #b9b9b9;
            }
            body {
              background-color: rgb(26, 26, 26);
            }
            </style></head><body><table>
        """)

        # Header
        f.write('<tr><th>Object</th><th>Prefix</th><th>Ids</th><th>Suffix</th></tr>')

        # Results
        for r in results:
            row_class = 'ok' if r['pass'] else 'error'
            f.write(
                f"<tr class=\"{row_class}\"><td>{r['object']}</td><td>{r['prefix']}</td><td>{r['ids']}</td><td>{r['suffix']}</td></tr>")

        f.write('</table></body></html>')

    # Open Generated html Results
    webbrowser.open(out_html_file)

这是一段我借鉴来的代码。主要涉及到几部分内容。

  1. 这是一个class 的 CheckNamesOperator,也就是我们的检查器对象。
  2. 其中最核心的部分在 exeute() 内,用以运行刚刚的检查逻辑函数,并且把结果保存在results[] 这个list中
  3. 输出王爷的部分,其实这是一个调用IO的函数,主要是遍历刚刚我们保存下来的results 数组并且逐行按照我们的目标效果编译出来的,中间还涉及到html 和css代码(虽然挺丑的)。最终把Html保存在Blender相同的目录下面。

检查代码效果

创建几个object,并且根据我们的目标规则,更改命名。可以看到场景内对对错错的object有好几个,虽然我不是专业的测试,但是基础的边界测试还是要有的。

image-20230405194512646

检查完自动弹出的检查结果窗口,很丑,但是可以提示我们哪里有错误,可以及时修改。

image-20230405194614938

再写一个Pie Menu的Object生成菜单

插件的逻辑

  1. View3D_MT_PIE_template(menu) 用以继承官方的pie menu并且在下面添加我们自己的功能按钮,用pie.operator()
  2. View3D_OT_PIE_template_call(menu) 用以在场景内呼出这个菜单
  3. 每一个功能按钮单独用一个class实现,bl_idname就是在pie.operator()中调用的id, 每一个功能按钮都有一个execute用以运行。其中的功能可以自行设计,我这里就调用比如生成plane, cube, sphere的代码
  4. 额外编写一个add_hotkey()用以实现快捷键呼出菜单的功能,核心是这一段,用以调用场景内呼出菜单的效果。
km.keymap_items.new(
    VIEW3D_OT_PIE_template_call.bl_idname, 'D', 'PRESS', ctrl=True, shift=False)
  1. 最后的最后,把全部实现的功能都注册在register()和unregister()中就可以了。

插件在Blender中效果实现如下

完整插件代码如下,可以直接复制并且安装在blender中,默认是shift + D呼出菜单。

import bpy
from bpy.types import Menu
''' 
Reference List 
1. https://www.youtube.com/watch?v=jfQTX293dw0
2. https://blender.stackexchange.com/questions/147894/is-there-a-way-to-add-a-custom-pie-menu-in-2-8
3. https://docs.blender.org/manual/en/latest/advanced/scripting/addon_tutorial.html
'''


bl_info = {
    "name": "Kampter Pie Menu",
    "author": "Kampter",
    "version": (0, 0, 0, 1),
    "description": "Pie Menus made by Kampter",
    "blender": (3, 40, 0),
    "category": "Mesh"
}

addon_keymaps = []


def add_hotkey():
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon

    if not kc:
        print('Keymap Error')
        return
    # object Mode
    km = kc.keymaps.new(name='Object Mode', space_type='EMPTY')
    # here you can chose the keymapping.
    kmi = km.keymap_items.new(
        VIEW3D_OT_PIE_template_call.bl_idname, 'D', 'PRESS', ctrl=True, shift=False)
    addon_keymaps.append((km, kmi))


def remove_hotkey():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)

    addon_keymaps.clear()


class VIEW3D_MT_PIE_template(Menu):
    bl_label = 'Kampter Mesh Generator Menu'

    def draw(self, context):
        layout = self.layout
        prefs = context.preferences
        inputs = prefs.inputs

        pie = layout.menu_pie()
        pie.operator('kampter.plane')
        pie.operator('kampter.cube')
        pie.operator('kampter.sphere')
        pie.operator('kampter.cylinder')


class VIEW3D_OT_PIE_template_call(bpy.types.Operator):
    bl_idname = 'kampter.call'
    bl_label = 'Kampter Pie Menu'
    bl_description = 'Calls pie menu'
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_template")
        return {'FINISHED'}


class CREATE_PLANE(bpy.types.Operator):
    bl_idname = 'kampter.plane'
    bl_label = 'Plane'

    def execute(self, context):
        bpy.ops.mesh.primitive_plane_add(enter_editmode=False, align='WORLD', location=(-2.25724, -2.46012, 2.15026),
                                         scale=(1, 1, 1))
        return {'FINISHED'}


class CREATE_CUBE(bpy.types.Operator):
    bl_idname = 'kampter.cube'
    bl_label = 'Cube'

    def execute(self, context):
        bpy.ops.mesh.primitive_cube_add(enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
        return {'FINISHED'}


class CREATE_UV_SPHERE(bpy.types.Operator):
    bl_idname = 'kampter.sphere'
    bl_label = 'UV Sphere'

    def execute(self, context):
        bpy.ops.mesh.primitive_uv_sphere_add(radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0),
                                             scale=(1, 1, 1))
        return {'FINISHED'}


class CREATE_CYLINDER(bpy.types.Operator):
    bl_idname = 'kampter.cylinder'
    bl_label = 'Cylinder'

    def execute(self, context):
        bpy.ops.mesh.primitive_cylinder_add(enter_editmode=False, align='WORLD',
                                            location=(-0.492159, -3.12005, -1.62732), scale=(1, 1, 1))
        return {'FINISHED'}


def register():
    bpy.utils.register_class(VIEW3D_MT_PIE_template)
    bpy.utils.register_class(VIEW3D_OT_PIE_template_call)
    bpy.utils.register_class(CREATE_PLANE)
    bpy.utils.register_class(CREATE_CUBE)
    bpy.utils.register_class(CREATE_UV_SPHERE)
    bpy.utils.register_class(CREATE_CYLINDER)
    add_hotkey()


def unregister():
    bpy.utils.unregister_class(VIEW3D_MT_PIE_template)
    bpy.utils.unregister_class(VIEW3D_OT_PIE_template_call)
    bpy.utils.unregister_class(CREATE_PLANE)
    bpy.utils.unregister_class(CREATE_CUBE)
    bpy.utils.unregister_class(CREATE_UV_SPHERE)
    bpy.utils.unregister_class(CREATE_CYLINDER)
    remove_hotkey()


if __name__ == "__main__":
    register()

Reference

  1. blender插件开发入门 - 知乎 (zhihu.com)
  2. https://www.youtube.com/watch?v=jfQTX293dw0
  3. https://blender.stackexchange.com/questions/147894/is-there-a-way-to-add-a-custom-pie-menu-in-2-8
  4. https://docs.blender.org/manual/en/latest/advanced/scripting/addon_tutorial.html
Comments