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.

```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:

| 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`.