从零构建通用 3D 编辑器:导入导出与数据序列化的实战之路

3 阅读5分钟

💡 写在前面
为了让大家更好地本地体验编辑器,目前我们已开源了除 SDK 核心和 AI 功能以外的所有模块。 编辑器在线体验地址:editor.moyunhe.com/resource-pl…

欢迎下载体验,如果觉得有用,顺手给个 Star ⭐ 是对我们最大的鼓励!


image.png

🎯 一、设计哲学:搭积木式的功能整合

进入正题。一个通用的 3D 编辑器,看似基础的功能点其实暗藏玄机:

  • 核心能力:模型加载、场景保存、灯光/环境球/背景调整。
  • 进阶能力:场景压缩、后处理管线、相机配置、物体与材质属性精细调整。
  • 灵魂能力撤销/重做系统 以及 灵活的插件系统,界面UI与3D属性快速整合的能力。

这些功能单看都不难,但要让它们 “好用”“易于与前端交互” ,就需要精心的架构设计。

🧱 我的核心设计哲学

“除了核心渲染循环之外,其他所有模块必须是可拆分、可组合、可删减、可替换的。”

只有做到极致的模块化,整个设计工具才具备最佳的扩展性可成长性。在不改变核心代码的前提下,通过插件无限扩展编辑器能力,这才是我们追求的目标。


📥 二、模型加载:曲线救国的智慧

1. 痛点:BabylonJS 的原生局限

BabylonJS 对模型格式的支持相对“高冷”。官方最推荐的是 GLB/GLTF 格式。
对于 FBX, STL, B3DM 等工业常用格式,原生支持较弱。如果逐一去实现这些格式的解析器,工作量巨大且难以维护。

2. 解决方案:借力 Three.js 生态

经过多年在 Three.js 和 BabylonJS 之间的摸索,我发现了一个绝佳的“曲线救国”方案:
利用 threepipe 项目。

  • 优势:这是一个集成度极高的开源项目,它魔改了 Three.js 底层,集成了多种模型格式的转换功能。

  • 策略:虽然不建议直接将其用于商业生产(因为修改了 Three.js 底层,存在作者停更导致项目瘫痪的风险),但作为 “模型格式转换中间件” 简直完美,我们未在3D SDK本身集成这个转换,是在前端中引入了这个转换流程,后续有更好的转换方式也可以接入。

  • 工作流

    1. 利用 threepipe 将 FBX/OBJ/STL 等格式统一转换为标准的 GLB 模型。
    2. 将生成的 GLB 交给 BabylonJS 进行加载和渲染。

💡 提示:熟悉后端的朋友也可以自行搭建转换服务,核心思路一致:统一输入源为 GLB

3. 模型加载关键代码:注册 GLTF Loader

BabylonJS 引擎底层本身不默认包含 GLTF 解析器,需要显式导入并注册:

typescript

编辑

// 1. 显式导入 Loader
import { GLTFFileLoader } from "loaders/glTF/glTFFileLoader";

// 2. 全局注册插件,这一步式代码内部实现的,这里拿出来只是分析原理
RegisterSceneLoaderPlugin(new GLTFFileLoader());

// 3. 统一调用入口
// 引擎内部会自动识别格式,如果是 glb/gltf 则自动使用上述 Loader
SceneLoader.LoadAssetContainerAsync(...);

📤 三、场景导出:不仅仅是保存文件

导出功能同样不是 BabylonJS 的原生“开箱即用”能力,我们需要引入 serializers 库并进行深度定制。

1. 基础导出逻辑

这是导出的核心异步方法,负责生成 GLB 数据:

typescript

编辑

public static async GLBAsync(
  scene: Scene, 
  fileName: string, 
  options?: IExportOptions
): Promise<GLTFData> {
    // 等待场景准备就绪
    if (!options || !options.exportWithoutWaitingForScene) {
        await scene.whenReadyAsync();
    }

    const exporter = new GLTFExporter(scene, options);
    // 生成 GLB 二进制数据
    const data = await exporter.generateGLBAsync(fileName.replace(/.[^/.]+$/, ""));
    
    exporter.dispose();
    return data;
}

2. 自动压缩:Draco 的福音

在 BabylonJS 8.0 版本之前,我们需要额外引入 gltf-transform 来处理 Draco 压缩。但现在,一切变得简单了。

  • 内置支持GLTFExporter 内部自带了 KHR_draco_mesh_compression 扩展。
  • 无需手动调用:导出时会自动启用 Draco 压缩网格数据;导入时也会自动识别并解压。

Babylonjs导出文件截图:

image.png

Babylonjs导入文件结构:

image.png

🧩 四、深度定制:如何收集“非标准”数据?

官方的 Exporter 和 Serializers 只能处理标准 GLTF 数据。但作为一款编辑器,我们需要保存更多“私有”信息:

  • 📷 相机数据:视角、焦距、轨道位置。
  • 💡 灯光数据:自定义强度、颜色、阴影参数。
  • 🎨 材质扩展:自定义 Shader 参数、SSS 设置。
  • 🌍 环境配置:环境球 (HDR)、背景渐变、地面投影设置。
  • 🔌 插件状态:后处理开关、UI 布局状态等。

1. 挑战与维护

为了获取这些数据,我们不得不将 BabylonJS 的 serializers 和 loaders 源码复制到项目内部进行魔改

很多人看到“复制源码”就头大,担心难以维护。其实不然:

  • 最小化修改:我们只修改与 Material (材质)、Mesh (网格) 相关的序列化逻辑。
  • 隔离依赖:大部分扩展逻辑无需变动,只需调整引入 BabylonJS 核心库的方式即可。

image.png

2. 数据结构设计

为了实现模块化,我将导出数据清晰地分为两类:

🌐 全局场景数据 (Global Scene Data)

  • 相机配置
  • 场景自定义元数据
  • 背景设置 (Background)
  • 环境球 (Environment Map)

📦 局部模型数据 (Local Model Data)

  • 自定义材质参数
  • 模型变换与属性
  • 插件专属数据 (地面投影、灯光特效、后处理链状态等)

✨ 五、神器登场:装饰器模式简化序列化

面对如此分散的数据,如何高效收集?难道要写几百个 JSON.stringify
当然不。BabylonJS 底层强大的 装饰器 (Decorators)  机制是我们的救星。

1. 定义即收集

对于每一个需要导出的属性,我们只需添加一个简单的装饰器标记:

image.png

2. 一行代码完成序列化

有了装饰器,序列化过程变得极其优雅:

typescript

编辑

toJSON(): any {
    // 利用 SerializationHelper 自动收集所有带装饰器的属性
    const serializationObject = SerializationHelper.Serialize(this);
    
    // 附加类型标识,方便反序列化时识别
    serializationObject.type = (this as any).constructor.PluginType;
    serializationObject.assetType = "config";
    
    // 触发事件,通知外部系统
    this.dispatchEvent({ type: "serialize", data: serializationObject });
    
    return serializationObject;
}

3. 轻松复原 (反序列化)

恢复数据同样简单,框架会自动匹配并填充:

typescript

编辑

fromJSON(data: any): this | null | Promise<this | null> {
    if (!this._viewer?.scene) {
      return null;
    }
    // 类型校验
    if (data.type !== this.constructor.PluginType) return null;
    
    // 自动解析并赋值
    SerializationHelper.Parse(() => this, data, this._viewer?.scene);
    
    this.dispatchEvent({ type: "deserialize", data });
    return this;
}

✅ 效果:通过这套机制,原本可能需要上千行的数据收集代码,现在几百行即可搞定,且类型安全,易于维护,上面提到的不同类别的场景信息,都可以使用装饰器的方式收集和恢复。


🖼️ 六、进阶优化:图片资源的存储策略

在序列化过程中,背景图环境球 (HDR)  是两个特殊的“体积杀手”。
如果处理不当,一个原本几百 KB 的模型文件,加上这两张大图后,瞬间膨胀到几 MB 甚至几十 MB。

我们有两种处理思路:

方案 A:外链存储 (强烈推荐 ⭐)

  • 做法:将背景图和环境球图片上传到 CDN 或对象存储,序列化数据中只保存 URL 链接
  • 优点:极大减少 GLB 文件体积,加载速度快,便于资源复用。
  • 适用:绝大多数 Web 应用场景。

方案 B:内嵌二进制 (BufferView)

  • 做法:将图片转换为二进制流,像普通纹理一样存储到 GLB 的 bufferView 中。

  • 实现逻辑

    typescript

    编辑

    private _exportImage(name: string, mimeType: ImageMimeType, data: ArrayBuffer): number {
        const images = this._exporter._images;
        let image: IImage;
    
        if (this._exporter._shouldUseGlb) {
            // GLB 模式:存入 BufferView
            image = {
                name: name,
                mimeType: mimeType,
                bufferView: undefined, // 后续由 BufferManager 填充
            };
            const bufferView = this._exporter._bufferManager.createBufferView(new Uint8Array(data));
            this._exporter._bufferManager.setBufferView(image, bufferView);
        } else {
            // GLTF + 外部图片模式
            const baseName = name.replace(/./|/|.\|\/g, "_");
            const extension = GetFileExtensionFromMimeType(mimeType);
            let fileName = baseName + extension;
            
            // 防止文件名冲突
            if (images.some((image) => image.uri === fileName)) {
                fileName = `${baseName}_${Tools.RandomId()}${extension}`;
            }
    
            image = { name: name, uri: fileName };
            this._exporter._imageData[fileName] = { data: data, mimeType: mimeType };
        }
    
        images.push(image);
        return images.length - 1;
    }
    
  • 优点:单文件交付,无需担心外链失效。

  • 缺点:文件体积大,解析慢。


🗜️ 七、终极压缩:WebP 格式改造

模型文件的体积主要由两部分决定:

  1. 几何信息:已通过 Draco 完美解决。
  2. 图片纹理:这是剩下的瓶颈。

众所周知,WebP 格式在同等画质下体积远小于 PNG/JPG。我们需要改造 GltfExporter 以支持 WebP 输出。别担心,利用 BabylonJS 的工具函数非常简单:

typescript

编辑

// 使用 DumpTools 进行异步格式转换
const webpData = await DumpTools.DumpDataAsync(
  size.width, 
  size.height,
  data,             // 原始图像数据
  "image/webp",     // 目标格式
  undefined,
  false,
  true,
  quality           // 质量系数
) as ArrayBuffer;

⚖️ 质量系数 (Quality) 的选择

  • 常规纹理:设置为 0.85。肉眼几乎无损,体积减小显著。

  • 法线贴图 (Normal Map)强烈建议设置为 1.0

    ⚠️ 注意:法线贴图对精度极其敏感,0.85 的压缩可能会导致光照计算出现伪影或噪点,务必保留最高质量。


🏁 八、结语

以上就是我关于整个编辑器导入、导出、压缩、场景数据收集以及图片优化的核心实现思路。

从模型格式的“曲线救国”,到利用装饰器优雅地解决复杂数据序列化,再到极致的体积压缩,每一步都是为了让开发者能从繁琐的底层细节中解脱出来,专注于创意本身。

当然,将一个编辑器打磨到极致是一个庞大的工程,本文仅抛砖引玉,列出了最关键的技术路径。如果你有更巧妙的思路,或者在实施过程中遇到了有趣的挑战,欢迎在评论区一起交流!

👉 再次提醒:完整代码已开源,欢迎 Star 支持!
GitHub: blueRaining/QDesigner-Public

整套工具源码也支持出售,用于训练你的专属WEB3D的skills 下一章开始一一介绍,如何优雅的设计UI和3D交互

欢迎加入我们的交流群,一起探索 Web 3D 的无限可能!

image.png

欢迎加入我们的知识星球,知识星球将会拥有全套的技术细节实现讲解!

image.png