在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 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定制技术的进阶分享!