mxmxmx2tiles 0.1.0

Convert Bentley ContextCapture 3mx photogrammetry data to Cesium 3D Tiles
Documentation

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 .3mx and 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.

cargo build --release

Usage

mxmxmx2tiles <scene.3mx> <output-dir>

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

python3 validate.py <output-dir> [glbs-per-area]

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/jpg resources are handled; 3mx xyz point-cloud buffers are not (none of the target data uses them).
  • CTM decoding (via the openctm crate) implements the MG1 method that ContextCapture emits; RAW and MG2 are not implemented.

License

Apache-2.0 — see LICENSE.