多色 3MF 为什么在别的软件里变成灰模型:解码 Bambu Studio 的 paint_color

一句话结论:Bambu 3MF 里的多色并没有丢,只是它不按多数 3D 软件期望的方式存——一部分是"对象 → 丝材槽"的分配,另一部分是直接写在三角形上的 paint_color,而后者是一个十六进制序列化的细分树,要从最后一个字符往前读。看懂这个编码,浏览器里也能把多色模型正确渲染出来。

想直接看模型效果?用 3D 模型在线查看器 在浏览器里打开 3MF / STL / OBJ / PLY / glTF / FBX 等格式,核对几何、朝向与颜色。

本文完整编译自 dev.to 文章《How Bambu Studio stores multi-color painting in a 3MF — decoding paint_color in the browser》(2026-10-15),并做中文整理。


一、现象:颜色在,但显示不出来

从 MakerWorld 下载一个多色模型,用 Bambu Studio 以外的几乎任何查看器打开 .3mf,你都会得到一个灰色(或单色)块。但颜色确实在文件里——只是不是多数 3D 软件期望的存法。

背景:Bambu 3MF 的几何用 3MF production extension 分散在 3D/Objects/*.model 里。要先把它 flatten(摊平)、拿到三角形,才谈得上上色。(这也是 PrusaSlicer 打不开 Bambu 3MF 的原因。)

二、Bambu 3MF 里的两种颜色

1)逐对象丝材(Per-object filament)

每个对象(或部件)被分配到一个丝材槽,记录在 Metadata/model_settings.config:

<object id="2">
  <metadata key="name" value="Cat"/>
  <metadata key="extruder" value="3"/>
</object>

实际颜色在 Metadata/project_settings.config——它虽然叫 .config 其实是 JSON:

"filament_colour": ["#202020", "#C12E1F", "#FF6A13", ...],
"filament_type":   ["PLA", "PLA", "PLA", ...]

所以 extruder = 3 表示"用 filament_colour[2]"(注意是 0 起始)。当每种颜色是独立部件时,只做这一步就能得到正确着色的模型。

2)逐三角面涂色(Per-triangle painting)

当有人在 Bambu Studio(或 Orca)里用画笔画时,颜色被写到三角形本身上,作为一个额外属性:

<triangle v1="10" v2="11" v3="12" paint_color="4"/>
<triangle v1="12" v2="13" v3="10" paint_color="8"/>
<triangle v1="14" v2="15" v3="16" paint_color="4C"/>
<triangle v1="16" v2="17" v3="18"/>

不带该属性的三角形,使用对象自己的丝材。 PrusaSlicer 用完全相同的编码,只是换了名字:slic3rpe:mmu_segmentation——因为 Bambu Studio 的这套逻辑是从 PrusaSlicer 的 TriangleSelector fork 出来的。

三、解码那串十六进制

paint_color 不是颜色,也不只是一个槽号。它是一个序列化的细分树(subdivision tree)。

原理:当你在一个三角形的一部分上涂色时,切片器会把该三角形切成更小的三角形并给它们上色。这串字符存下整棵树,每个节点一个十六进制半字节(nibble)。

有两处不寻常:

  • 读字符串要从最后一个字符往前读;
  • 每个 nibble 打包两个字段:低 2 位是分裂边数,高 2 位是颜色(对叶子节点而言)。
nibble = 0b SS CC         (从右往左逐个读)
        CC = 低 2 位 = split 边数
        SS = 高 2 位 = state(当该节点是叶子时)

split == 0  → 叶子
   SS < 3   → state = SS
   SS == 3  → state = (下一个 nibble) + 3     // 槽位 ≥ 3 的转义
split > 0   → 该节点有 split + 1 个子节点;递归解码

state 0 表示"未涂色,用对象自己的丝材";state n 表示丝材槽 n。

一些来自真实 MakerWorld 模型的取值:

paint_color 读作 槽位
4 0100:叶子,SS=1 1
8 1000:叶子,SS=2 2
0C C=1100:叶子,SS=3,转义 → 下一 nibble 0 + 3 3
4C 转义 → 4 + 3 7
44043 3:分裂成 4 个子节点 → 4,0,4,4 混合:1,0,1,1

对应的 TypeScript 解码函数:

export function decodePaintState(hex: string): number {
  let pos = hex.length - 1;
  const next = () => (pos < 0 ? 0 : parseInt(hex[pos--], 16) || 0);
  const weight = new Map<number, number>();

  const visit = (w: number, depth: number) => {
    const code = next();
    const split = code & 0b11;
    if (split === 0 || depth > 16) {
      const state = (code & 0b1100) === 0b1100 ? next() + 3 : code >> 2;
      weight.set(state, (weight.get(state) ?? 0) + w);
      return;
    }
    for (let i = 0; i < split + 1; i++) visit(w / (split + 1), depth + 1);
  };

  visit(1, 0);
  // 占比最大的颜色胜出
  let best = 0, bestW = -1;
  for (const [s, w] of weight) if (w > bestW) { best = s; bestW = w; }
  return best;
}

四、作者做的简化(以及为什么)

忠实的渲染器会重建细分后的三角形并逐个上色。但对一个查看器来说,不需要这么做。作者的做法是:给每个原始三角形,取覆盖它最多的那个叶子状态的颜色,每级分裂时子节点等权。

这是近似,但数据支持它够用:

  • 在测试的模型上(221,124 个三角形、7 种丝材),只有少部分三角形被分裂,大多数是 4 / 8 这样的单一叶子;
  • 整个文件解码在浏览器里约 60 ms;
  • 视觉上只在颜色边界丢掉子三角形细节,因此边界可能略显锯齿。

五、接入 three.js

ThreeMFLoader 会丢弃未知属性,所以作者自己用正则扫 XML(在这个体量下足够快),收集每个对象的 Uint8Array 状态,再把它和 loader 生成的 mesh 对上:

  1. 按三角形数量 + 首个顶点位置匹配(loader 保持顶点顺序);
  2. 把几何转成 non-indexed,让每个三角形拥有自己的 3 个顶点;
  3. 写 color 属性:state 0 → 对象丝材,state n → filament_colour[n-1];
  4. 用白色材质 + vertexColors: true。

一个坑:three.js 期望 color 属性在线性空间,但丝材颜色是 sRGB 十六进制。new THREE.Color("#C12E1F") 会替你完成转换(自 r152 起颜色管理默认开启);如果你自己解析 hex 字节再除以 255,就跳过了这一步,颜色会发白。

六、给做 3D 打印与查看器的团队

  • 多色 3MF 的"单一灰模"多半是查看器方言问题,不是文件损坏:先确认它是否支持 production extension 与 paint_color;
  • extruder 与 paint_color 是两层机制:前者是对象级,后者是面级;只实现前者,画笔涂的区域会全丢;
  • 从后往前读、每 nibble 拆两段是这套编码最容易写错的地方;有真实取值表可以对照自测;
  • 近似解码对查看器足够,但如果要做切片或统计用丝量,就必须做忠实重建;
  • 颜色空间是浏览器 3D 的常见坑:sRGB ↔ 线性转换别自己手写。

常见问题

Q:为什么 PrusaSlicer 打不开 Bambu 的 3MF?几何分散在 3D/Objects/*.model(production extension),需要先 flatten。本编码与涂色机制则与 PrusaSlicer 同源。

Q:paint_color 是颜色值吗?不是。它是细分树的序列化,代表"哪些子三角形属于哪个丝材槽"。

Q:为什么我只看到灰模,但 Bambu Studio 里是彩色的?你的查看器没实现 paint_color(面级涂色),或没读取 filament_colour。颜色数据一直都在。

Q:解码一定要重建细分三角形吗?查看器不必;做切片/用量统计则需要。


来源: - How Bambu Studio stores multi-color painting in a 3MF — decoding paint_color in the browser(dev.to,2026-10-15) - 前作:Why PrusaSlicer can't open your Bambu 3MF…- 3MF 规范:https://3mf.io/

相关阅读:STL 模型格式详解 | STL 模型修复权威指南 | STEP 模型在线查看 | CAD / 制造 / G-Code 完全指南

本文由 BimAnt 编译整理,转载请注明出处。最后更新:2026-10-15