Model Formats¶
SceneView loads a model from a path, a URI or a byte buffer. The format is detected from
the bytes, not from the file name — a file shared into your app with no extension and a
application/octet-stream MIME type still opens.
| Format | Extensions | Where it works | Notes |
|---|---|---|---|
| glTF / GLB | .gltf, .glb |
Android · Web · Desktop · TV · Flutter and React Native on Android | The native format of the Filament platforms. Skeletal and morph animations, PBR materials, Draco and WebP textures. Not on Apple: RealityKit reads no glTF, so SceneViewSwift's ModelFormat leaves .gltf / .glb out on purpose — convert with tools/convert-usdz.sh. |
| USDZ / Reality / USD | .usdz, .reality, .usd, .usda, .usdc |
Apple (iOS · macOS · visionOS), and Flutter / React Native on iOS | Loaded by RealityKit through SceneViewSwift, already in metres. |
| STL | .stl (binary + ASCII) |
Android · Apple | Android converts to GLB in memory; Apple reads it through ModelIO. No unit in the file: millimetres assumed. |
| OBJ | .obj (+ .mtl) |
Android · Apple | Android converts to GLB with material colours; Apple reads it through ModelIO with its sidecar .mtl. |
| PLY | .ply (binary + ASCII) |
Android · Apple | Android converts to GLB; both keep vertex colours. |
| 3MF | .3mf |
Android · Apple | Android converts to GLB in memory (parser in sceneview-core, Kotlin Multiplatform); Apple parses it in SceneViewSwift. Declared units are honoured on both. |
On Apple, ModelFormat.sniff(contentsOf:) decides the format from the bytes first and the
extension second, and MeshAsset.load(contentsOf:) opens the STL / OBJ / PLY / 3MF files.
3MF — the format AI print flows emit¶
Ask ChatGPT for a 3D print from a drawing and it hands back a .3mf: an OPC/ZIP package
whose 3D/3dmodel.model part is XML, with <vertices>, <triangles>, <components> and a
<build> plate, in millimetres and Z-up. It is what every slicer and every image-to-print
flow produces — and until SceneView read it, no Android app opened one in 3D, let alone in
AR.
There is no new API¶
ModelLoader sniffs the payload by its ZIP magic and converts it to GLB before the bytes
reach Filament, so every entry point you already use accepts a 3MF:
val modelLoader = rememberModelLoader(engine)
SceneView(modifier = Modifier.fillMaxSize(), engine = engine, modelLoader = modelLoader) {
rememberModelInstance(modelLoader, "print.3mf")?.let {
ModelNode(modelInstance = it, scaleToUnits = 1.0f)
}
}
A file shared in from another app works the same way — pass the content:// URI as a
string:
rememberModelInstance(modelLoader, uri.toString())?.let {
ModelNode(modelInstance = it, scaleToUnits = 1.0f)
}
A payload that is not a ZIP costs one four-byte comparison, so the check is free for the glTF path.
Receiving a file from another app¶
The Android demo declares ACTION_VIEW and ACTION_SEND filters for .3mf, .glb and
.gltf, so it appears in the app chooser and the share sheet
(#3510). Two things the platform forces,
if you do the same in your own app:
- Sniff the bytes, not the metadata. A
.3mfsent through the share sheet arrives withapplication/octet-streamas its type and with no queryable display name — both signals blank. A name-or-MIME check refuses a file the SDK reads perfectly. - Read the ClipData, not a bare
EXTRA_STREAM. A sender'sFLAG_GRANT_READ_URI_PERMISSIONcoversgetData()and the ClipData only; read the extra of a share that carries both andopenInputStreamreturnsnull.
samples/android-demo/src/main/java/io/github/sceneview/demo/OpenedModel.kt is the working
implementation.
What the conversion does¶
| Step | Why |
|---|---|
| Millimetres → metres | 3MF carries real-world size (unit="millimeter" unless stated otherwise). A 60 mm print is life-size in AR without a magic number. |
| Z-up → Y-up | 3MF uses the printer's build-plate axes; glTF and SceneView use Y-up. The part stands up instead of lying on its back. |
| Flat normals | 3MF stores no normals. Faces are de-indexed and given per-face normals — flat shading is what a printed part looks like, and a smoothed normal would round over the facets the slicer will extrude. |
| Colours → glTF materials | <basematerials> and the materials extension's <colorgroup> become one glTF material per colour, per object and per triangle. |
doubleSided |
Generated meshes are often inconsistently wound, and a one-sided print renders inside-out. |
Unrecognised 3MF extensions (slice, beamlattice, production) are skipped, never rejected — an unknown extension must not stop a print from being previewed.
Custom pipelines¶
The parser is pure Kotlin in sceneview-core — no java.util.zip, no XML library, no
expect/actual — so it runs on every Kotlin target:
if (ThreeMfLoader.isThreeMf(bytes)) {
val model = ThreeMfLoader.parse(bytes) // objects, meshes, build items
val glb = ThreeMfLoader.toGlb(bytes) // ready for any glTF consumer
}
Roadmap¶
The 3MF parser is deliberately dependency-free so the same shape carries the rest of the formats a print or scan pipeline emits. These are tracked and not yet shipped — check the issue before relying on one:
| Format | Issue |
|---|---|
| STL (binary + ASCII) | #3486 |
| PLY (mesh, vertex colours) | #3487 |
| OBJ + MTL | #3488 |
One ModelFormat entry point (supportedFormats, unit, typed errors) |
#3489 |
| Open-with for every format in the Android demo | #3490 |
Every format on the web (loadModel, /open, the view_3d_model widget) |
#3491 |
See also¶
llms.txt— the 3MF section in the machine-readable API reference- Nodes Reference — what to do with a model once it is loaded
- Quickstart — the shortest path to a rendered model