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 version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
<Default Extension="png" ContentType="image/png"/>
</Types>
_rels/.rels
<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Target="/3D/3dmodel.model" Id="rel-1"
Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
</Relationships>
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 version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xml:lang="en-US"
xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:BambuStudio="http://schemas.bambulab.com/package/2021"
xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06"
requiredextensions="p">
<metadata name="Application">BambuStudio-02.06.01.55</metadata>
<metadata name="BambuStudio:3mfVersion">1</metadata>
<!-- other optional metadata (Copyright, CreationDate, …) -->
<resources>
<object id="2" type="model">
<components>
<component p:path="/3D/Objects/object_1.model" objectid="1"
transform="1 0 0 0 1 0 0 0 1 0 0 0"/>
</components>
</object>
</resources>
<build>
<item objectid="2" printable="1"/>
</build>
</model>
"From Bambu Lab" detection
Bambu Studio 2.5+ shows "not from Bambu Lab, load geometry data only" unless both of
these <metadata> elements are present in 3D/3dmodel.model:
<metadata name="Application">BambuStudio-XX.XX.XX.XX</metadata>
<metadata name="BambuStudio:3mfVersion">1</metadata>
The exact version string in Application does not matter; only the prefix BambuStudio-
is checked.
3D/_rels/3dmodel.model.rels
<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Target="/3D/Objects/object_1.model" Id="rel-1"
Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
</Relationships>
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 version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xml:lang="en-US"
xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:BambuStudio="http://schemas.bambulab.com/package/2021"
xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06">
<metadata name="BambuStudio:3mfVersion">1</metadata>
<resources>
<object id="1" type="model">
<mesh>
<vertices>
<vertex x="0.0000" y="0.0000" z="-3.0000"/>
…
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2" paint_color="4"/>
<triangle v1="3" v2="4" v3="5" paint_color="1C"/>
…
</triangles>
</mesh>
</object>
</resources>
</model>
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)
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 <object id> in 3D/3dmodel.model.
<?xml version="1.0" encoding="UTF-8"?>
<config>
<object id="2">
<metadata key="name" value="my model"/>
<metadata key="extruder" value="1"/>
<metadata face_count="474884"/>
<part id="1" subtype="normal_part">
<metadata key="name" value="my model"/>
<metadata key="matrix" value="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1"/>
<metadata key="extruder" value="1"/>
</part>
</object>
<plate>
<metadata key="plater_id" value="1"/>
<metadata key="locked" value="false"/>
</plate>
</config>
The extruder value here sets the base filament for the object. Per-triangle
overrides come from paint_color.
Metadata/project_settings.config (optional)
A large JSON file containing machine profile, filament profiles, and print settings.
Bambu Studio saves the full machine gcode here. For third-party generators, the
minimum useful subset is filament_colour:
{
"filament_colour": ["#B4A082", "#468CD2", "#50A550", "#DCD7C8"]
}
Order corresponds to filament slots 1–4. Bambu Studio uses these hex colours to display each painted region in the viewport before slicing. If the file is absent, Bambu Studio uses the colours from the loaded filament profiles instead.
Observations and caveats
Separate-object approach doesn't work for multi-colour. Writing separate
<object>elements (one per colour) and using<basematerials>/pindexattributes causes Bambu Studio 2.5+ to load geometry but show all parts with the default (first) filament colour. Thepaint_colorper-triangle approach is required.requiredextensions="p"on the<model>element of3D/3dmodel.modelis needed for the Production Extension namespace (xmlns:p). Without it some validators reject the file, though Bambu Studio loads it either way.Vertex indices are 0-based in
<triangle>elements (same as standard 3MF; OBJ uses 1-based).Coordinates are in millimetres with the origin at the model's natural zero; Bambu Studio auto-centres on the print plate on import.
The geometry object file can be inlined directly into
3D/3dmodel.model(skipping theobject_1.modelsplit and the.relsfile), but Bambu Studio's own exporter always uses the split form, so the split form is the safer choice for compatibility.
Unresolved: project_settings.config and the preset-name dialog
Including Metadata/project_settings.config with a filament_colour array causes Bambu
Studio to display the correct per-slot colours in the viewport. However it also triggers
a "customised filament or printer presets" dialog on every import, and filament slots
2–4 are named model.3mf (the project filename) rather than something meaningful.
Root cause
Bambu Studio matches filament_settings_id entries against its installed preset database.
When a preset name is not found it creates an embedded preset named after the source file.
The first slot gets its name from filament_settings_id[0]; slots 2–4 appear to fall back
to the filename regardless of what filament_settings_id[1..3] contains.
Preset names are machine-specific (e.g. "Bambu PLA Basic @BBL A1") so a third-party
generator cannot supply correct names without knowing the user's exact machine model and
installed preset database.
Things tried that did not help
| Attempt | Result |
|---|---|
filament_colour only |
Correct colours, dialog shows —, slots unnamed |
filament_settings_id: [""] |
Slots named model.3mf |
filament_settings_id: ["Generic PLA", ...] |
Slots named model.3mf |
filament_settings_id: ["Terrain","Water","Parks","Roads"] |
Slot 1 named Terrain(model.3mf), rest model.3mf |
filament_settings_id: ["Terrology"×4] |
Slot 1 named Terrology(model.3mf), rest model.3mf |
Practical outcome
The paint_color encoding is correct and colours display properly. The dialog and naming
are cosmetic nuisances only. A future fix would require either:
- Detecting the user's Bambu machine model and looking up the corresponding preset name, or
- Bambu Lab exposing a machine-agnostic generic filament preset name that suppresses the dialog.