3D模型在DCC工具、游戏引擎和Web端之间流转时,几何数据、材质贴图通常能保留下来,但自定义属性却经常在导出导入后消失。这类元数据包括物体ID、交互参数、物理质量、LOD层级、遮挡标记、音频索引等。丢失的根本原因不是单个软件缺陷,而是格式规范对自定义字段的支持不一致,以及导出器默认配置忽略了这类数据。要解决这个问题,不能只依赖某一个软件的隐藏选项,而是需要从资产管线层面设计一套可落地的自定义属性存储与读取机制。

一、元数据丢失的常见原因与格式差异
不同3D格式对自定义属性的支持程度差异巨大。OBJ格式本质上只包含顶点、法线、UV和面索引,完全没有任何元数据槽位;STL同样只保存几何信息。如果项目还在使用OBJ作为中间交换格式,自定义属性必然全部丢失。FBX虽然支持自定义属性,但它的实现依赖Autodesk SDK,不同DCC软件对FBX自定义属性的写入方式并不完全一致,Blender写入的自定义属性在Unity中读取时可能被重命名或直接忽略。USD格式功能强大,支持customData和自定义schema,但学习成本和管线改造成本较高,很多中小团队并不适用。glTF作为Web端和实时渲染领域的主流格式,在2.0规范中正式提供了extras字段,允许在任意对象上附加任意JSON数据,这成为目前最轻量、最开放的解决方案。
除了格式本身的能力限制,导出器行为也是丢失元数据的重要原因。以Blender导出glTF为例,如果用户没有在导出面板中勾选自定义属性相关选项,glTF文件里根本不会出现extras字段。同样,Unity导出FBX时,如果自定义数据没有挂载在MonoBehaviour上,而是放在模型导入设置里,导出后也会丢失。因此排查元数据丢失问题时,首先要确认源文件中的自定义属性是否真正被序列化到目标格式中,而不是仅仅停留在编辑器内存里。
二、glTF extras:标准化的自定义属性容器
glTF规范允许在asset、scene、node、mesh、material、texture等几乎任意对象上附加extras字段。extras的值必须是一个合法的JSON对象,可以包含字符串、数字、布尔值、数组和嵌套对象。它的设计初衷就是为应用特定的数据提供一个不破坏格式兼容性的存储空间。一个典型的glTF文件根节点结构如下:
{
"asset": {
"version": "2.0",
"generator": "Blender glTF Exporter"
},
"scene": 0,
"scenes": [
{
"name": "MainScene",
"nodes": [0]
}
],
"nodes": [
{
"name": "Chair",
"mesh": 0,
"extras": {
"customId": "chair_001",
"mass": 2.5,
"tags": ["furniture", "indoor"],
"interaction": {
"type": "pickup",
"range": 1.8
}
}
}
],
"extras": {
"author": "ipipp",
"version": "1.0.0"
}
}
在上面的结构中,根节点的extras用于存放资产级信息,而具体节点的extras则保存该物体的业务属性。读取端只需要解析JSON就能恢复自定义数据。需要注意,extras中的内容必须可以被JSON序列化,不能包含函数、类实例或循环引用。如果原始属性中使用了Python对象或Unity组件,导出前需要先转换成基础类型。
extras的另一个优势是向后兼容。不符合规范的读取器通常会忽略未知字段,因此即使目标平台不支持extras,也不会导致加载失败。但这也带来一个隐患:某些引擎的导入器可能直接丢弃extras而不做任何提示。所以在选择glTF作为元数据载体时,最好在管线的测试阶段就用一个小模型验证读取端是否真的能拿到extras内容。
三、Blender与Three.js的完整读写示例
在Blender中给模型添加自定义属性非常直观。用户可以在属性面板中手动添加,也可以通过Python脚本批量设置。下面的脚本会给当前选中的物体写入两个自定义属性,一个用于业务ID,另一个用于物理质量。
import bpy obj = bpy.context.active_object obj["custom_id"] = "chair_001" obj["mass"] = 2.5 obj["interaction_type"] = "pickup"
写入完成后,导出glTF时需要确保自定义属性被包含。Blender的glTF导出器在导出面板中提供了数据选项,其中包含自定义属性相关的开关。不同版本的Blender可能将开关命名为Custom Properties或Attributes,导出前需要确认勾选。如果使用命令行或脚本导出,可以通过参数控制。导出的glTF文件中,这些属性会以extras的形式出现在对应节点下。
在Three.js中读取glTF模型时,GLTFLoader默认会解析extras,但获取方式并不总是直观。根级extras可以通过解析器对象访问,而节点级extras需要遍历场景图后从userData或对象属性中读取。下面是一个完整的读取示例:
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load('model.glb', (gltf) => {
// 读取资产级extras
const rootExtras = gltf.parser.json.extras;
console.log('root extras:', rootExtras);
// 遍历场景中的物体,读取节点级extras
gltf.scene.traverse((child) => {
if (child.isMesh) {
const nodeExtras = child.userData.extras;
if (nodeExtras) {
console.log(child.name, nodeExtras);
// 根据自定义属性执行业务逻辑
if (nodeExtras.customId === 'chair_001') {
child.userData.mass = nodeExtras.mass;
}
}
}
});
});
注意遍历时读取的是child.userData.extras,这是因为GLTFLoader会把节点extras挂载到对应Object3D的userData中。如果模型使用Draco压缩或Meshopt压缩,extras仍然会保留,不受几何压缩影响。但如果是通过某些第三方工具二次转换后的glb文件,extras可能被剥离,所以最好在进入最终打包流程前做一次数据校验。
如果需要在Three.js中重新导出带自定义属性的glTF,可以在创建物体时直接写入userData,然后使用GLTFExporter导出。该导出器会将userData中可序列化的字段写入extras。示例代码如下:
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
const mesh = new THREE.Mesh(geometry, material);
mesh.name = 'Chair';
mesh.userData.customId = 'chair_001';
mesh.userData.mass = 2.5;
const exporter = new GLTFExporter();
exporter.parse(
mesh,
(result) => {
// result为ArrayBuffer,可保存为glb文件
const blob = new Blob([result], { type: 'application/octet-stream' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'chair.glb';
link.click();
},
(error) => {
console.error('export error', error);
},
{ binary: true }
);
四、跨格式通用方案:伴生JSON与资产管理
如果项目需要同时支持glTF、FBX、USD等多种格式,仅依赖某一格式的扩展字段并不够。此时可以采用伴生JSON方案。核心思路是:模型文件本身只负责几何和材质,所有自定义元数据统一存放在与模型同名的JSON文件中,运行时通过资源加载器将两者关联起来。例如模型文件为chair.glb,伴生文件为chair.meta.json,目录结构如下:
{
"modelFile": "chair.glb",
"customId": "chair_001",
"mass": 2.5,
"interaction": {
"type": "pickup",
"range": 1.8
},
"lodLevels": [0, 1, 2],
"occlusionCulling": true
}
读取时先加载JSON,再加载模型,根据节点名称或索引将元数据合并到场景对象上。这种方式完全不依赖特定3D格式的扩展能力,任何导出管线都可以接入。缺点是文件数量增加,且需要额外的加载逻辑来保证元数据与模型同步。对于已经使用AssetBundle或Addressables的项目,可以把元数据作为单独的Asset加载,与模型资源建立依赖关系,避免手动管理文件对应。
对于更大规模的资产管线,建议引入资产管理数据库或资源表。模型在导出时只携带一个稳定的资源ID,所有业务属性、物理参数、交互配置都存储在数据库或配置表中。运行时根据ID查询元数据并注入到场景中。这种方案将3D格式和业务数据彻底解耦,迁移到新的引擎或格式时不需要重新导出模型,只需要更新数据库映射。代价是需要维护额外的服务或配置系统,对小型项目来说可能过重。
综合来看,如果团队以glTF为主要格式,优先使用extras字段是最直接的方案。如果需要跨DCC和引擎且格式多样,伴生JSON配合资源ID是更稳妥的选择。无论采用哪种方式,关键是在管线早期就建立元数据规范,并加入自动化校验,避免模型进入生产环境后才发现自定义属性已经悄悄丢失。