mxmxmx2tiles
This is slop coded.
Convert Bentley ContextCapture 3mx photogrammetry data to Cesium 3D Tiles (glTF content, georeferenced), so it can be served as static files and viewed in any open 3D Tiles client (CesiumJS, iTowns, …).
This exists because 3mx is a Bentley format that open web viewers don't read, and the only direct 3mx web viewer is Bentley's proprietary (licensed) Acute3D viewer. 3mx and 3D Tiles are structurally near-identical (an LOD tree of nodes with a bounding volume, a screen-space-error threshold, mesh + texture, and children), so the conversion is a faithful 1:1 remap rather than a re-tiling.
What it does
scene.3mx ──► tileset.json + a tree of *.glb
| 3mx | → | 3D Tiles |
|---|---|---|
.3mxb node (3MXBO container) |
→ | one tile |
| OpenCTM mesh + JPEG | → | glTF .glb, JPEG passed through untouched |
node bbMin/bbMax |
→ | boundingVolume.box |
node maxScreenDiameter |
→ | geometricError (= diag / msd × 16) |
node children (child files) |
→ | child tiles |
SRS + SRSOrigin |
→ | root tile transform (ENU → ECEF) |
- Georeferencing. The scene's SRS is read from the
.3mxand the projected coordinates are transformed to ECEF, expressed in a single ENU frame at the scene origin so glTF vertices stay small and float32-precise. Any Transverse Mercator grid works — the TM parameters (origin lat/lon, scale, false easting/northing, ellipsoid) are parsed from the SRS WKT, so Korea belts, UTM zones, and most national grids are handled; non-TM projections error cleanly. The TM↔geodetic math is hand-rolled (Redfearn series) and unit-tested against PROJ to sub-cm — no libproj dependency. Datum shift is assumed zero. - Unlit materials. Photogrammetry meshes carry no normals and bake lighting
into the texture, so materials use
KHR_materials_unlit(full-brightness texture), matching ContextCapture's own 3D Tiles export. - Portable orientation. glTF content is emitted Y-up (the default 3D Tiles
convention) rather than relying on the Cesium-specific
gltfUpAxis, so output renders correctly in Y-up-only loaders (three.js / iTowns) too. - UVs are V-flipped (
v → 1-v): 3mx/OpenCTM use OpenGL's bottom-left UV origin, glTF uses top-left.
Build
Pure Rust, no unsafe, no system libraries.
Usage
The output dir gets a tileset.json and a mirror of the source tile tree as
.glb. Serve it over HTTP (3D Tiles needs HTTP, not file://) and point any
3D Tiles client — CesiumJS, iTowns, … — at the root tileset.json.
Layout
src/tmxb.rs 3MXBO container parser
src/geo.rs Transverse Mercator SRS -> ECEF / ENU (validated against PROJ)
src/glb.rs glTF/GLB writer (unlit, JPEG passthrough)
src/meshopt_glb.rs meshopt-compressed glTF/GLB writer
src/tileset.rs recursive 3mx-tree -> 3D Tiles tree
src/main.rs CLI
src/spike.rs single-tile debug tool (one .3mxb -> one .glb)
validate.py structural validator (GLB/glTF 2.0 + 3D Tiles 1.1 invariants)
CTM mesh decoding lives in the separate openctm crate.
Validating output
Walks every tileset.json (geometricError monotonicity, box format, URI
resolution) and deep-checks a sample of glbs (accessor bounds, alignment,
index range, embedded-JPEG validity). Dependency-free.
Compression
Geometry is meshopt-compressed (EXT_meshopt_compression), supported by CesiumJS
and three.js (MeshoptDecoder). Three modes:
| Mode | Geometry | Env |
|---|---|---|
| lossless (default) | exact float32, meshopt-coded | — |
| quantize | uint16 (KHR_mesh_quantization, sub-mm loss, smallest) |
TMX_QUANTIZE=1 |
| raw | uncompressed glTF | TMX_RAW=1 |
JPEG textures pass through unchanged and dominate the output (they are already compressed), so the mode mostly affects the geometry share. On the reference dataset: ~61 GB raw → ~44 GB lossless → ~40 GB quantized.
Notes / limitations
- Textures are the size floor (passthrough JPEG); shrinking them further needs lossy re-encoding or KTX2/BasisU (a runtime/VRAM win more than a disk win).
- Only
geometryBuffer/ctm+textureBuffer/jpgresources are handled; 3mxxyzpoint-cloud buffers are not (none of the target data uses them). - CTM decoding (via the
openctmcrate) implements the MG1 method that ContextCapture emits; RAW and MG2 are not implemented.
License
Apache-2.0 — see LICENSE.