3MF Exporter
What is 3MF?
3MF (3D Manufacturing Format) is an open, XML-based file format designed for additive manufacturing and 3D printing workflows. Unlike formats such as STL, 3MF is a versatile container that carries geometry, units, materials, colors, metadata, and structural relationships in a single zip-based package (OPC — Open Packaging Conventions). It is natively supported by most modern slicers, CAD tools, and manufacturing pipelines.
Because 3MF is designed to faithfully represent manufacturing intent, a Babylon.js scene may need to be prepared before exporting. For example, you may want to apply world transforms, merge or split meshes by material, assign meaningful names, set a specific unit, or strip rendering-only data that has no manufacturing meaning. The exporter works directly on the meshes you pass to it, so you remain in full control of what goes into the file.
Installation and prerequisites
The serializer relies on fflate for zip compression. By default it loads fflate lazily from a CDN:
// Override the CDN URL before creating any serializer instance (optional)import { ThreeMfSerializerGlobalConfiguration } from "babylonjs-serializers";ThreeMfSerializerGlobalConfiguration.FFLATEUrl = "https://cdn.example.com/fflate.js";If fflate is already present on globalThis.fflate (bundled by your application), the dynamic load is skipped automatically.
Basic usage
Serialize to a memory buffer
The simplest way to export one or more meshes is ThreeMf.SerializeToMemoryAsync. It buffers the entire 3MF archive into a Uint8Array that you can then save or upload.
import { ThreeMfSerializer, ThreeMf } from "babylonjs-serializers";
const serializer = new ThreeMfSerializer();
const buffer = await ThreeMf.SerializeToMemoryAsync(serializer, mesh1, mesh2);
if (buffer) { // Save as a file in the browser const blob = new Blob([buffer], { type: "application/octet-stream" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "model.3mf"; a.click(); URL.revokeObjectURL(url);}Stream to a custom sink
For large scenes or environments where you want to avoid buffering the entire archive in memory (e.g., writing directly to a Node.js file stream), use serializeAsync with a sink callback:
import { ThreeMfSerializer } from "babylonjs-serializers";import { createWriteStream } from "fs";
const serializer = new ThreeMfSerializer({ unit: ST_Unit.millimeter });const chunks: Uint8Array[] = [];
await serializer.serializeAsync( (err, chunk, final) => { // append chunk to your stream / buffer chunks.push(chunk); }, mesh1, mesh2);Serializer options
Options are passed as an object to the ThreeMfSerializer constructor. All fields are optional.
| Option | Type | Default | Description |
|---|---|---|---|
unit | ST_Unit | ST_Unit.meter | Unit used for all coordinates in the exported file. Must match the scale of your scene. |
exportInstances | boolean | false | When true, InstancedMesh objects are exported as additional build items that reference the shared geometry of their source mesh — no geometry duplication. When false, instances are silently ignored. |
metadata | Record<string, string> | undefined | Arbitrary key/value pairs written as model-level metadata. Standard keys include Title, Designer, Description, Copyright, LicenseTerms, CreationDate, ModificationDate, and Application. |
Units
The ST_Unit enum provides the full set of units supported by the 3MF specification:
import { ST_Unit } from "babylonjs-serializers";
// Available valuesST_Unit.micronST_Unit.millimeter // most common for desktop 3D printingST_Unit.centimeterST_Unit.inchST_Unit.footST_Unit.meter // default for ThreeMfSerializerImportant: The unit does not rescale coordinates. It declares to the consuming application how to interpret the raw numbers in the file. Make sure the unit you choose matches the actual scale of your mesh vertices.
Example: millimeter export with metadata
import { ThreeMfSerializer, ST_Unit, ThreeMf } from "babylonjs-serializers";
const serializer = new ThreeMfSerializer({ unit: ST_Unit.millimeter, metadata: { Title: "My Part", Designer: "Jane Doe", Description: "Bracket assembly for prototype v2", CreationDate: new Date().toISOString().slice(0, 10), },});
const buffer = await ThreeMf.SerializeToMemoryAsync(serializer, bracket);Example: exporting instances
When a scene contains many copies of the same object created with Mesh.createInstance(), enabling exportInstances avoids duplicating geometry for every copy:
const serializer = new ThreeMfSerializer({ exportInstances: true });
// sourceMesh is exported once as a resource object.// each InstancedMesh becomes a separate build item pointing to that object.const buffer = await ThreeMf.SerializeToMemoryAsync(serializer, sourceMesh, ...sourceMesh.instances);Preparing your scene for export
Because 3MF targets manufacturing workflows, some aspects of a typical Babylon.js scene may need attention before exporting:
- Apply transforms: The serializer uses each mesh's current world matrix. If you want a "baked" export with no runtime transform, call
mesh.bakeCurrentTransformIntoVertices()beforehand. - Units and scale: If your scene is in centimeters but the slicer expects millimeters, either rescale the meshes or choose
ST_Unit.centimeterso the consumer can convert correctly. - Submeshes and materials: Each Babylon.js
SubMeshis exported as its own 3MF object resource. This allows downstream tools to assign per-object colors or materials. If you want a single object per mesh, merge submeshes before exporting. - Names: Object names in the exported file come from
mesh.name. Give meshes meaningful names to make the 3MF easier to work with in slicer or CAD software. - Non-manifold geometry: 3MF consumers typically require watertight (manifold) geometry for printing. Validate and repair meshes as needed before export.
Advanced: using the builders directly
The serializer internally uses a set of fluent builder classes. You can use these directly if you need to construct a 3MF file from raw geometry data (independent of any Babylon.js mesh):
import { ThreeMfModelBuilder, ThreeMfMeshBuilder, ThreeMfMaterialBuilder, ThreeMfDocumentBuilder, ST_Unit,} from "babylonjs-serializers";
// 1. Define a material groupconst material = new ThreeMfMaterialBuilder(1) .withColor("Red", { r: 0.8, g: 0.1, b: 0.1 }) .withColor("Blue", { r: 0.1, g: 0.1, b: 0.8 }) .build();
// 2. Define a mesh resource (positions: flat x,y,z array; indices: flat i,j,k array)const mesh = new ThreeMfMeshBuilder(2) .withName("Cube") .withData({ positions: [...], indices: [...] }) .withMaterial(1, 0) // reference material group id=1, entry index 0 .build();
// 3. Assemble the modelconst model = new ThreeMfModelBuilder() .withUnit(ST_Unit.millimeter) .withMetaData("Title", "My Model") .withMaterial(material) .withMesh(mesh) .withBuild(2) // add mesh id=2 to the build list .build();
// 4. Wrap in an OPC documentconst document = new ThreeMfDocumentBuilder() .withModel(model) .build();Builder reference
| Builder | Purpose |
|---|---|
ThreeMfModelBuilder | Assembles the root 3MF model: unit, metadata, resources (meshes + materials), and build items. |
ThreeMfMeshBuilder | Creates a mesh resource object from a flat positions array and a flat indices array. Accepts an optional vertex/triangle post-process handler. |
ThreeMfMaterialBuilder | Creates a basematerials resource group. Colors are expressed as linear RGB and converted to sRGB hex automatically. |
ThreeMfComponentsBuilder | Creates a composite object made of references to other resource objects with per-component transforms. |
ThreeMfDocumentBuilder | Wraps a model into a full OPC document (content types + relationships + model part). |
ThreeMfMeshBuilder post-process handlers
If you need to transform individual vertices or triangles during export (e.g., to remap coordinates or inject per-triangle property references), you can attach handlers:
const mesh = new ThreeMfMeshBuilder(id) .withData(data) .withPostProcessHandlers( (vertex) => { // transform or annotate the vertex return vertex; }, (triangle) => { // assign per-triangle material references triangle.pid = materialGroupId; triangle.p1 = 0; return triangle; } ) .build();