续篇:Draco压缩+贴图外链!模型34MB→4MB的终极优化

86 阅读5分钟

续篇:Draco压缩+贴图外链!模型34MB→4MB的终极优化

书接上回,我们通过「格式修复→GPU实例化→Feature ID绑定→Mesh合并」四步优化,将BIM模型的Draw Call从1388骤降到26,实现了丝滑渲染和精准拾取。

但优化后34MB的模型体积仍有优化空间,且实际部署时贴图与模型绑定的方式不够灵活。

本篇将聚焦「Draco几何压缩+贴图外联」方案,解决部署痛点的同时,让模型体积再降88%,最终从34MB压缩至4MB!

一、直接压缩的“坑”:gltf-pipeline -d -t 报错实录

在上一篇的Step4后,我们得到了34MB的output_merged.glb(含Feature ID和顶点颜色),为了进一步压缩体积并实现贴图独立部署,直接执行了标准压缩命令:

gltf-pipeline -i output_merged.glb -o O_dec.glb -d -t

其中:

  • -d:启用Draco网格压缩(KHR_draco_mesh_compression),核心用于压缩顶点、索引等几何数据
  • -t:启用贴图外联(separateTextures),将模型内嵌入的贴图提取为独立文件(如Steelplt.png)

执行后直接报错,无法加载

Cesium控制台抛出两个关键信息:

  1. 核心报错TypeError: Cannot read properties of undefined (reading 'count')

    1. 报错位置:InstancingPipelineStage.process(Cesium3DTileset渲染管线)
    2. 直接导致模型加载失败,无法渲染
  2. 警告信息Accessor xxx for instanced attribute "_FEATURE_ID_0" is not defined in the glTF file. This attribute will be skipped.

    1. 提示实例化属性(TRANSLATION/ROTATION/_FEATURE_ID_0)对应的Accessor未定义
    2. 原因指向gltf-pipeline压缩时误删了“未使用”的Accessor

image.png

问题本质:索引更新遗漏

经过排查,发现并非Accessor被误删,而是gltf-pipeline的核心逻辑缺陷:

  1. Draco压缩会在accessors数组末尾追加新的元数据Accessor,随后删除原始未使用的Accessor
  2. 删除过程中,会对primitive.attributesanimation.samplers等常规字段的Accessor索引做前移更新(如删除索引2后,索引3及以上均减1)
  3. 关键遗漏node.extensions.EXT_mesh_gpu_instancing.attributes中的索引未同步更新(包含TRANSLATION/ROTATION/_FEATURE_ID_0对应的Accessor索引)
  4. 最终导致模型中引用的索引指向错位或不存在的Accessor,Cesium访问accessor.count时拿到undefined报错

二、修复方案:补充索引更新逻辑

需修改gltf-pipeline的removeUnusedElements.js文件,在删除Accessor时同步维护实例化属性的索引。

修改文件路径

lib/removeUnusedElements.js

修改函数

Remove.accessor(在现有更新animation索引的逻辑后、函数结尾前新增)

新增代码

// Update accessor ids for EXT_mesh_gpu_instancing (instanced attributes)
if (usesExtension(gltf, "EXT_mesh_gpu_instancing")) {
  ForEach.node(gltf, function (node) {
    if (
      defined(node.extensions) &&
      defined(node.extensions.EXT_mesh_gpu_instancing)
    ) {
      const attrs = node.extensions.EXT_mesh_gpu_instancing.attributes;
      Object.keys(attrs).forEach(function (key) {
        if (attrs[key] > accessorId) {
          attrs[key]--;
        }
      });
    }
  });
}

修复逻辑说明

  • 当模型使用EXT_mesh_gpu_instancing扩展时,遍历所有节点
  • 对每个节点的实例化属性(TRANSLATION/ROTATION/_FEATURE_ID_0等)的Accessor索引进行检查
  • 若索引值大于被删除的Accessor ID,则索引减1,与accessors数组的splice操作保持同步
  • 确保Cesium渲染时能正确找到实例化属性对应的Accessor,报错彻底解决

三、新问题:贴图外联的“命名乱象”

修复索引问题后,执行压缩命令能成功导出独立贴图,但发现新问题:同名贴图会自动追加序号

  • 例如模型中有3个同名的“Steelplt.png”贴图,导出后会生成Steelplt.pngSteelplt_1.pngSteelplt_2.png
  • 实际业务中,这些同名贴图往往是重复资源或需要统一替换的材质,多份文件既占用存储空间,也增加部署复杂度

image.png

优化方案:新增同名贴图替换选项

通过扩展gltf-pipeline的配置项,新增replaceSameNameTextures参数,支持同名贴图覆盖替换。

修改涉及3个核心文件

1. lib/writeResources.js
  • 新增参数说明:

        * @param {boolean} [options.replaceSameNameTextures=false] When saving separate textures, overwrite existing file when name conflicts instead of appending _1, _2.
    
  • 初始化参数:

        options.replaceSameNameTextures = options.replaceSameNameTextures ?? false;
    
  • 修改同名处理逻辑:

      const name = getName(gltf, object, index, extension, options);
      relativePath = name + extension;
      // For textures: optionally overwrite same name; otherwise append a number
      const isImage = extension !== ".bin" && extension !== ".glsl";
      if (
        !(isImage && options.replaceSameNameTextures) &&
        defined(options.separateResources[relativePath])
      ) {
        let number = 1;
        while (defined(options.separateResources[relativePath])) {
          relativePath = `${name}_${number}${extension}`;
          number++;
        }
      }
      return relativePath;
    
2. lib/processGltf.js
  • 新增参数说明和默认配置:
* @param {boolean} [options.replaceSameNameTextures = false] When using separate textures, overwrite same-named files instead of appending _1, _2.
    ......
    options.replaceSameNameTextures = options.replaceSameNameTextures ?? defaults.replaceSameNameTextures;
    ......
    processGltf.defaults = {
        separateTextures: false,
        /**
         * When using separate textures, overwrite same-named image files instead of appending _1, _2.
         * @type Boolean
         * @default false
         */
        replaceSameNameTextures: false,
     };
3. bin/gltf-pipeline.js
  • 新增命令行选项:
replaceSameNameTextures: {
    describe: "When using -t, overwrite same-named texture files instead of appending _1, _2.",
    type: "boolean",
    default: defaults.replaceSameNameTextures,
},
  • 传递参数:
replaceSameNameTextures: argv.replaceSameNameTextures,

最终可用命令

gltf-pipeline -i output_merged.glb -o final.glb -d -t --replaceSameNameTextures
  • 效果:同名贴图不再生成序号后缀,后写入的贴图直接覆盖前序文件,最终只保留一份同名贴图
  • 优势:减少冗余文件,贴图部署时可直接替换同名文件实现批量更新

四、终极效果:34MB → 4MB的质变

经过上述修复和优化,模型实现了“功能无损+体积暴降”的双重目标:

阶段核心操作体积关键变化
优化后中间态Mesh合并+Feature ID34.86MBDraw Call=26,支持精准拾取
最终优化态Draco压缩+贴图外联+同名替换4.12MB体积压缩88%,贴图独立部署

关键优化亮点

  1. 体积优化:34MB→4MB,主要得益于Draco对几何数据的高效压缩(顶点、索引压缩率超70%)
  2. 部署灵活:贴图独立导出为PNG/JPG,可单独进行CDN加速、格式优化(如转为KTX2)
  3. 功能无损:实例化渲染、Feature ID拾取功能完全保留,Cesium加载无报错,帧率稳定在30+FPS
  4. 资源精简:同名贴图自动合并,避免冗余,部署包体积进一步减小

总结

从1388 Draw Call到26,再从34MB到4MB,BIM模型的优化完成了“性能+体积”的双重闭环:

  • 前序四步优化解决“渲染卡顿”和“无法拾取”的核心痛点
  • 本篇 Draco压缩+贴图外联解决“体积过大”和“部署不便” 的落地问题

最终交付的4MB模型,既满足了Web端快速加载的需求,又保留了BIM模型的业务交互能力(构件拾取、属性查询),完全适配Cesium等3D地理信息平台的生产环境使用。

下面是最终效果,能够实现构件拾取,交互帧率基本恒定在58~60FPS。 screenshot_2026-02-04_17-00-29.gif