【Python】Blender插件开发教程:在Blender中扩展GLTF导出功能,支持自定义数据字段(GLTF Extensions完整实现+Python源码分享)_blender gltf导出插件
在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 插件使用
假设已经安装并启用插件,完整使用流程如下:
-
在文件 - 导出 - Export GLTF with Custom Extension中点击
-
弹出输入窗口
-
填写扩展名称,例如:
my_metadata
-
添加若干个字段,比如:
- author → “Ikkyu”
- version → “v1.0.0”
- isInteractable → “true”
-
选择保存路径,命名文件
-
点击确认
-
得到带有
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
导出菜单) - author与description清楚说明插件作者和功能
这是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方法控制每一行的绘制
- 使用两列分别显示
key
和value
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 bpyimport jsonimport osfrom bpy.types import Operator, Panel, PropertyGroup, UIListfrom bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty, EnumPropertybl_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_propsif __name__ == \"__main__\": register()
七、结语
通过这篇分享,你不仅掌握了如何开发一个完整的Blender导出插件,还了解了以下核心技能:
- Blender Python API基本使用
- 属性系统(PropertyGroup、CollectionProperty)
- 界面绘制(UIList、Popup Dialog)
- 文件操作(JSON读写)
- 插件注册与集成流程
希望这篇文章能对你在Blender插件开发、GLTF自定义工作流中带来实际帮助!
如果你觉得这篇内容对你有用,欢迎点赞收藏,后续我还会持续更新更多Blender脚本开发和GLTF定制技术的进阶分享!