# 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
```
| `.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.
```sh
cargo build --release
```
## Usage
```sh
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`](../openctm) crate.
## Validating output
```sh
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:
| **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`.