# Bambu Studio 3MF format — reverse-engineered spec
Documented from a real Bambu Studio 2.06.01.55 save file and OrcaSlicer source code.
Relevant to any tool that wants to generate `.3mf` files that Bambu Studio accepts natively.
---
## File structure (ZIP)
```
[Content_Types].xml
_rels/.rels
3D/3dmodel.model ← assembly/metadata (small)
3D/_rels/3dmodel.model.rels ← points to the object file
3D/Objects/object_1.model ← geometry (large)
Metadata/model_settings.config
Metadata/project_settings.config ← optional; sets filament colours
Metadata/plate_1.png ← optional; thumbnail
```
The geometry is split into a separate file referenced via a relationship. Both model files
use the same XML namespace set.
---
## `[Content_Types].xml`
```xml
```
---
## `_rels/.rels`
```xml
```
---
## `3D/3dmodel.model` — assembly / build manifest
Contains metadata and a single top-level object that references the geometry file via
a component. No geometry lives here.
```xml
BambuStudio-02.06.01.551
```
### "From Bambu Lab" detection
Bambu Studio 2.5+ shows **"not from Bambu Lab, load geometry data only"** unless both of
these `` elements are present in `3D/3dmodel.model`:
```xml
BambuStudio-XX.XX.XX.XX1
```
The exact version string in `Application` does not matter; only the prefix `BambuStudio-`
is checked.
---
## `3D/_rels/3dmodel.model.rels`
```xml
```
---
## `3D/Objects/object_1.model` — geometry
Contains the actual mesh. Multi-colour is encoded **per triangle** using the `paint_color`
attribute (see below), not via separate objects or material groups.
```xml
1
```
---
## `paint_color` encoding
The `paint_color` hex string on each triangle is a serialized **OrcaSlicer/PrusaSlicer
`TriangleSelector` leaf-node state** (the same format used by support painting and seam
painting, extended to 16 extruder slots for multi-material).
### `EnforcerBlockerType` enum (from `TriangleSelector.hpp`)
```
NONE = 0 (no paint — inherits parent)
ENFORCER = 1 = Extruder1
BLOCKER = 2 = Extruder2
Extruder3 = 3
Extruder4 = 4
…
Extruder16 = 16
```
Extruder 1 and 2 reuse the `ENFORCER`/`BLOCKER` values for backward compatibility with
PrusaSlicer 2.3.1.
### Serialization (leaf triangle, no subdivision)
From `TriangleSelector::serialize()` in `TriangleSelector.cpp`:
```
bitstream = [split_sides & 1, split_sides & 2] # always [0, 0] for a leaf
+ state_bits
```
**State bits** (depends on `n = extruder_state_value`):
| n | State bits (LSB first) | Total bits |
|---|---|---|
| 0 (NONE) | `[0, 0]` | 4 |
| 1 (Extruder1) | `[1, 0]` | 4 |
| 2 (Extruder2) | `[0, 1]` | 4 |
| 3+ (Extruder3…) | `[1, 1]` + 4 bits of `(n-3)` LSB-first | 8 |
The bitstream is read as a **little-endian integer** (bit[0] = LSB) and formatted as an
**uppercase hex string**, padded to a whole nibble:
- 4-bit values → 1 hex digit (no leading zero)
- 8-bit values → 2 hex digits (leading zero if needed)
### Lookup table (extruders 1–6)
| Extruder | State (n) | Bitstream (LSB→MSB) | Integer | `paint_color` |
|---|---|---|---|---|
| 1 | 1 | `00 10` | 4 | `4` |
| 2 | 2 | `00 01` | 8 | `8` |
| 3 | 3 | `00 11 0000` | 12 | `0C` |
| 4 | 4 | `00 11 1000` | 28 | `1C` |
| 5 | 5 | `00 11 0100` | 44 | `2C` |
| 6 | 6 | `00 11 1100` | 60 | `3C` |
### General formula (Python)
```python
def paint_color(extruder: int) -> str:
"""extruder is 1-indexed (1 = first filament slot)."""
n = extruder
if n < 3:
return format(n * 4, 'X') # '4' or '8'
return format(12 + (n - 3) * 16, '02X') # '0C', '1C', '2C', …
```
---
## `Metadata/model_settings.config`
Per-object settings. The `object id` must match the `