【Python】Blender插件开发教程:在Blender中扩展GLTF导出功能,支持自定义数据字段(GLTF Extensions完整实现+Python源码分享)

#创意Python爱心代码分享#

在Blender中扩展GLTF导出功能:添加自定义数据字段

一、前言

在3D内容开发、游戏制作和XR应用中,GLTF(GL Transmission Format)逐渐成为主流的模型文件格式。它具有轻量、开放、跨平台的特点,并且被Unity、Unreal、Three.js、Babylon.js等广泛支持。
在实际开发中,我们常常需要在GLTF文件中附加一些自定义扩展数据(Custom Extensions),例如:配置信息、交互参数、元数据描述等。

那么,如何在Blender中,优雅地导出带有自定义扩展字段的GLTF文件呢?本文将以实战案例为核心,带你从零开发一个完整的Blender插件:
《GLTF Exporter:支持用户自定义扩展数据的导出器》

我们不仅会讲解核心代码,还会深入分析:

  • Blender插件结构
  • 属性系统(PropertyGroup)
  • 动态表单(UIList)
  • 文件读写与GLTF扩展注入
  • 弹窗交互设计(Popup Dialog)
  • 插件注册流程

文章全程干货,适合对Blender脚本开发感兴趣的开发者和有实际GLTF定制需求的团队阅读。


二、为什么要做GLTF自定义扩展导出?

在实际项目中,我们遇到的问题可能包括:

  • 希望给模型附加额外的行为参数(如动画速度、播放模式等)
  • 希望在模型中附加交互信息(如按钮点击区域、脚本回调)
  • 希望记录设计备注版本信息,方便后期维护

gltf 2.0 属性详解图
在这里插入图片描述
关注到Extensions属性
在这里插入图片描述

虽然GLTF规范允许通过 extensions 字段扩展任意内容,但Blender原生导出器并不支持自定义输入扩展数据

因此,开发一个插件来满足以下需求变得非常重要:

  • 自由定义扩展名称
  • 动态添加任意多的字段(Key-Value)
  • 简单直观的导出流程
  • 最少的侵入式修改,最大程度兼容原生GLTF结构

这正是我们要实现的目标。


三、插件使用体验展示

3.1 插件下载

可以直接从我分享的地址下载,也可从后文复制源码,保存为*.py文件

3.2 插件安装

安装步骤:

  • 在Blender中,选择“编辑”->“插件”->“从磁盘安装”。

在这里插入图片描述

  • 点击“安装”,选择刚才保存的 .py 文件。

  • 安装后,插件将在“文件”->“导出”菜单下显示为 Export GLTF with Custom Data。
    在这里插入图片描述

3.3 插件使用

假设已经安装并启用插件,完整使用流程如下:

  1. 文件 - 导出 - Export GLTF with Custom Extension中点击

  2. 弹出输入窗口

  3. 填写扩展名称,例如:my_metadata

  4. 添加若干个字段,比如:

    • author → “Ikkyu”
    • version → “v1.0.0”
    • isInteractable → “true”
  5. 选择保存路径,命名文件

  6. 点击确认
    在这里插入图片描述

  7. 得到带有extensions.my_metadata自定义数据的GLTF文件!

3.4 查看文件

gltf格式为采用json保存,因此我们可以在编辑器中查看文件

可见,扩展数据已经写入。
在这里插入图片描述

四、核心代码结构与模块详解

插件完整代码已经非常清晰,下面我们分模块进行讲解:

4.1 bl_info:插件元信息

bl_info = {
    "name": "GLTF Exporter",
    "blender": (2, 80, 0),
    "category": "Export",
    "author": "Ikkyu_tanyx_eqgis",
    "description": "Export GLTF with user-defined multiple custom extensions via a popup input window",
}
  • name:插件名称
  • blender:支持的Blender版本(这里是2.80+)
  • category:插件分类(这里归到Export导出菜单)
  • authordescription清楚说明插件作者和功能

这是Blender插件的标配,如果缺少,无法在插件管理器里正确显示。


4.2 定义数据模型(PropertyGroup)

为了让用户输入多个字段(key/value对),我们需要自定义数据结构:

GLTFExtensionField(单个字段)
class GLTFExtensionField(PropertyGroup):
    key: StringProperty(name="Key", default="")
    value: StringProperty(name="Value", default="")

每个字段有两个属性:

  • key:字段名
  • value:字段值
GLTFExtensionProperties(整体扩展信息)
class GLTFExtensionProperties(PropertyGroup):
    extension_name: StringProperty(...)
    fields: CollectionProperty(type=GLTFExtensionField)
    active_field_index: IntProperty(default=0)

包含:

  • extension_name:自定义扩展的名称
  • fields:字段集合
  • active_field_index:用于列表选中状态管理

小结:

使用PropertyGroup+CollectionProperty,能方便地在UI中动态管理一组数据对象,是Blender插件中常见且强大的模式。


4.3 定义UI显示(自定义UIList)

为了让字段编辑界面美观、直观,我们自定义了一个UIList控件:

class GLTF_UL_Fields(UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        row = layout.row()
        row.prop(item, "key", text="", emboss=True)
        row.prop(item, "value", text="", emboss=True)
  • draw_item方法控制每一行的绘制
  • 使用两列分别显示keyvalue
  • emboss=True让输入框更清晰

小结:

UIList是Blender官方推荐的动态表单控件,能灵活增删排序,非常适合类似表单管理的场景。


4.4 定义导出操作(Operator)

核心操作逻辑集中在ExportGLTFWithCustomDataOperator中。

invoke方法(弹出输入窗口)
def invoke(self, context, event):
    ...
    wm = context.window_manager
    return wm.invoke_props_dialog(self, width=600)

使用invoke_props_dialog弹出一个600宽度的对话框,让用户输入扩展信息。


draw方法(绘制输入表单)
def draw(self, context):
    layout.prop(props, "extension_name")
    layout.prop(self, "filename_ext")
    layout.template_list(...)
    ...
  • 输入扩展名称
  • 选择导出格式(.gltf/.glb)
  • 显示字段列表(动态增删字段)

还专门加了表头提示,用户体验更好!


execute方法(实际导出)
def execute(self, context):
    ...
    bpy.ops.export_scene.gltf(**export_kwargs)
    self.add_custom_extension(self.filepath, props.extension_name, props.fields)
  • 调用官方glTF导出命令
  • 导出后读取GLTF JSON内容
  • 插入自定义扩展数据
  • 保存覆盖原文件

add_custom_extension方法(插入扩展字段)
def add_custom_extension(self, filepath, extension_name, fields):
    ...
    gltf_data['extensions'][extension_name] = custom_data
  • 打开JSON文件
  • 添加或创建extensions字段
  • 写入新的自定义扩展
  • 保持原始格式缩进和中文兼容(ensure_ascii=False

小结:

通过直接读写glTF JSON,能最大化保留原始数据结构,且避免了复杂的二进制编辑。


4.5 辅助操作(添加/删除字段)

我们定义了两个小Operator来控制字段管理:

  • AddFieldOperator:添加一个空字段
  • RemoveFieldOperator:删除选中字段

非常简单直接,符合Blender插件交互逻辑。


4.6 插件注册与菜单集成

def register():
    ...
    bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
    ...
    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)

注册时,把导出功能挂到文件 - 导出菜单中,和官方导出器并列显示,用户容易找到。


五、可能的优化方向

虽然当前版本已经满足基本需求,但还有很多可以继续优化的方向:

  • 支持多层级嵌套扩展(目前只支持平铺的key-value)
  • 支持从已有GLTF文件读取现有扩展并编辑
  • 增加字段校验(比如确保Key合法性)
  • UI界面美化(比如支持图标、分组显示)

如果项目复杂度提高,可以进一步引入:

  • 自定义属性面板
  • 多语言支持
  • 预设管理功能(保存常用扩展模板)

源码分享

import bpy
import json
import os
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty, EnumProperty

bl_info = {
    "name": "GLTF Exporter",
    "blender": (2, 80, 0),
    "category": "Export",
    "author": "Ikkyu_tanyx_eqgis",
    "description": "Export GLTF with user-defined multiple custom extensions via a popup input window",
}

# 临时存储上一次保存目录
last_export_dir = ""

class GLTFExtensionField(PropertyGroup):
    key: StringProperty(name="Key", default="")
    value: StringProperty(name="Value", default="")

class GLTFExtensionProperties(PropertyGroup):
    extension_name: StringProperty(
        name="Extension Name",
        description="Name of the extension key",
        default="my_custom_extension"
    )
    fields: CollectionProperty(type=GLTFExtensionField)
    active_field_index: IntProperty(default=0)

class GLTF_UL_Fields(UIList):
    """显示字段列表"""
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        row = layout.row()
        row.prop(item, "key", text="", emboss=True)
        row.prop(item, "value", text="", emboss=True)

class ExportGLTFWithCustomDataOperator(Operator):
    bl_idname = "export_scene.gltf_custom_popup"
    bl_label = "Export GLTF with Custom Extension"
    bl_options = {'REGISTER', 'UNDO'}

    filename_ext: EnumProperty(
        name="Format",
        description="Choose the export format",
        items=[
            (".gltf", "glTF (.gltf)", "Export as glTF"),
            (".glb", "glTF Binary (.glb)", "Export as glb"),
        ],
        default=".gltf"
    )

    filepath: StringProperty(
        name="File Path",
        description="Path to save the exported GLTF",
        subtype='FILE_PATH'
    )

    def execute(self, context):
        global last_export_dir
        props = context.scene.gltf_extension_props

        # 自动补齐后缀 
        if not self.filepath.lower().endswith(self.filename_ext):
            self.filepath += self.filename_ext

        # 更新上次保存目录
        last_export_dir = os.path.dirname(self.filepath)

        # 导出
        export_kwargs = {
            'filepath': self.filepath,
            'export_format': 'GLB' if self.filename_ext == '.glb' else 'GLTF_SEPARATE',
        }
        bpy.ops.export_scene.gltf(**export_kwargs)

        # 修改glTF添加扩展
        self.add_custom_extension(self.filepath, props.extension_name, props.fields)

        self.report({'INFO'}, f"GLTF exported with custom extension to {self.filepath}")
        return {'FINISHED'}

    def invoke(self, context, event):
        global last_export_dir
        if last_export_dir and not self.filepath:
            # 默认带出上次保存目录
            self.filepath = os.path.join(last_export_dir, "untitled")
        wm = context.window_manager
        return wm.invoke_props_dialog(self, width=600)

    def draw(self, context):
        layout = self.layout
        props = context.scene.gltf_extension_props

        layout.prop(props, "extension_name")
        layout.prop(self, "filename_ext")

        # 添加 Key / Value 标题栏
        layout.label(text="Fields:")

        header = layout.row()
        header.label(text="Attribute")
        header.label(text="Value")

        row = layout.row()
        row.template_list("GLTF_UL_Fields", "", props, "fields", props, "active_field_index", rows=3)

        col = row.column(align=True)
        col.operator("gltf_extension.add_field", icon="ADD", text="")
        col.operator("gltf_extension.remove_field", icon="REMOVE", text="")

        layout.prop(self, "filepath")

    def add_custom_extension(self, filepath, extension_name, fields):
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                gltf_data = json.load(f)
        except Exception as e:
            print(f"Error reading glTF file: {e}")
            return

        if 'extensions' not in gltf_data:
            gltf_data['extensions'] = {}

        custom_data = {field.key: field.value for field in fields}
        gltf_data['extensions'][extension_name] = custom_data

        try:
            with open(filepath, 'w', encoding='utf-8') as f:
                json.dump(gltf_data, f, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"Error writing glTF file: {e}")

class AddFieldOperator(Operator):
    """添加一个新字段"""
    bl_idname = "gltf_extension.add_field"
    bl_label = "Add Field"

    def execute(self, context):
        props = context.scene.gltf_extension_props
        props.fields.add()
        return {'FINISHED'}

class RemoveFieldOperator(Operator):
    """删除选中的字段"""
    bl_idname = "gltf_extension.remove_field"
    bl_label = "Remove Field"

    def execute(self, context):
        props = context.scene.gltf_extension_props
        index = props.active_field_index
        if props.fields and index < len(props.fields):
            props.fields.remove(index)
            props.active_field_index = max(0, index - 1)
        return {'FINISHED'}

def menu_func_export(self, context):
    self.layout.operator(ExportGLTFWithCustomDataOperator.bl_idname, text="Export GLTF with Custom Extension")

def register():
    bpy.utils.register_class(GLTFExtensionField)
    bpy.utils.register_class(GLTFExtensionProperties)
    bpy.utils.register_class(GLTF_UL_Fields)
    bpy.utils.register_class(ExportGLTFWithCustomDataOperator)
    bpy.utils.register_class(AddFieldOperator)
    bpy.utils.register_class(RemoveFieldOperator)
    bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
    bpy.types.Scene.gltf_extension_props = PointerProperty(type=GLTFExtensionProperties)

def unregister():
    bpy.utils.unregister_class(GLTFExtensionField)
    bpy.utils.unregister_class(GLTFExtensionProperties)
    bpy.utils.unregister_class(GLTF_UL_Fields)
    bpy.utils.unregister_class(ExportGLTFWithCustomDataOperator)
    bpy.utils.unregister_class(AddFieldOperator)
    bpy.utils.unregister_class(RemoveFieldOperator)
    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
    del bpy.types.Scene.gltf_extension_props

if __name__ == "__main__":
    register()


七、结语

通过这篇分享,你不仅掌握了如何开发一个完整的Blender导出插件,还了解了以下核心技能:

  • Blender Python API基本使用
  • 属性系统(PropertyGroup、CollectionProperty)
  • 界面绘制(UIList、Popup Dialog)
  • 文件操作(JSON读写)
  • 插件注册与集成流程

希望这篇文章能对你在Blender插件开发、GLTF自定义工作流中带来实际帮助!
如果你觉得这篇内容对你有用,欢迎点赞收藏,后续我还会持续更新更多Blender脚本开发GLTF定制技术的进阶分享!


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EQ-雪梨蛋花汤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值