UntoldEngine ships two user-facing exporter commands in the scripts/ folder at the repo root:
export-untoldexport-untold-tiles
These wrappers launch Blender in background mode and run the Python exporters for you. Users should run the shell wrappers, not the raw Blender commands.
Blender must be installed.
The wrappers resolve Blender in this order:
--blender /path/to/BlenderBLENDER_BIN=/path/to/Blender/Applications/Blender.app/Contents/MacOS/BlenderblenderonPATH
If Blender cannot be found, the wrapper prints an install message and exits.
Use export-untold (found in scripts/) to convert one USD or USDZ asset into one .untold runtime file.
Basic usage:
./scripts/export-untold \
--input /path/model.usdz \
--output /path/model.untoldCommon options:
--input <path>: required source.usd,.usda,.usdc, or.usdz--output <path>: required destination.untold--file-type <tile|lod|hlod|shared>: optional, defaults totile--mesh-name <name>: optional, export only one mesh from a multi-mesh asset--ConvertOrientation: optional, convert the export into engine space--source-orientation <blender-native|engine-oriented>: optional, defaults toblender-native--validate: optional, also writes<name>.validation.json--blender <path>: optional wrapper-level Blender override
Example:
./scripts/export-untold \
--input GameData/Models/robot/robot.usdz \
--output GameData/Models/robot/robot.untold \
--ConvertOrientation \
--source-orientation blender-native \
--validateExpected output:
robot.untoldTextures/...beside the.untoldfile if the asset uses texturesrobot.validation.jsononly when--validateis passed
Use export-untold-tiles (found in scripts/) to partition a USD or USDZ scene into tile payloads and generate a manifest JSON file.
Basic usage:
./scripts/export-untold-tiles \
--input /path/scene.usdz \
--output-dir /path/tile_exports \
--tile-size-x 25 \
--tile-size-y 10000 \
--tile-size-z 25Common options:
--input <path>: required source.usd,.usda,.usdc, or.usdz--output-dir <path>: required destination directory for tile payloads--tile-size-x <number>: optional tile width in world units--tile-size-y <number>: optional tile height in world units, defaults to10000--tile-size-z <number>: optional tile depth in world units--auto-tile-size: optional automatic tile sizing--generate-hlod: optional HLOD generation--generate-lod: optional per-tile LOD generation--dry-run: optional planning pass without writing payload files--write-manifest-in-dry-run: optional manifest write during dry run--visible-only: optional export only visible meshes--all-meshes: optional include hidden meshes--debug-aabb-only: optional emit debug AABB payloads instead of geometry--quadtree: optional partition tiles using a quad-tree instead of a uniform grid--scene-profile <auto|indoor|outdoor>: optional streaming radius profile, defaults toauto. Radii are always proportional to scene size — no fixed distances to hand-tune. Useoutdoorfor cities, terrain, and large exterior scenes if auto-detection misses.--floor-count <number>: optional number of vertical floors to split each tile into--blender <path>: optional wrapper-level Blender override
Example:
./scripts/export-untold-tiles \
--input GameData/Models/dungeon/dungeon.usdz \
--output-dir GameData/Models/dungeon/tile_exports \
--tile-size-x 25 \
--tile-size-y 10000 \
--tile-size-z 25 \
--generate-hlod \
--generate-lodDry-run example:
./scripts/export-untold-tiles \
--input GameData/Models/dungeon/dungeon.usdz \
--output-dir GameData/Models/dungeon/tile_exports \
--tile-size-x 25 \
--tile-size-y 10000 \
--tile-size-z 25 \
--dry-run \
--write-manifest-in-dry-runExpected output layout:
dungeon.jsonbeside the tile payload directorytile_exports/tile_*.untold- optional HLOD and LOD
.untoldfiles intile_exports/ tile_exports/Textures/...for staged textures
The manifest stores relative runtime paths so it remains portable across machines, repos, and app bundles.
When MERGE_BY_MATERIAL is enabled (the default), objects that share the same material within a tile are joined into a single mesh entity before export. This reduces draw calls significantly, but means multiple original objects collapse into one exported entity — losing their individual names.
If you need certain objects to remain as separate identifiable entities (for example, to support tap-to-select workflows or per-object JSON lookups at runtime), prefix their name in Blender with NM_.
Objects whose name starts with NM_ are excluded from the merge step and exported individually, preserving their original name in the .untold file. All other objects are still merged normally.
Example naming in Blender:
NM_Pipe_001— exported as its own entity, name survives into.untoldNM_LightFixture_A— exported as its own entityWall_North— merged with other same-material walls, one entity for the groupDoor_Main— merged with same-material doors
This lets you keep background geometry (walls, floors, ceilings) optimized while still being able to identify and interact with specific objects at runtime:
To change the prefix or disable selective merging, edit NO_MERGE_PREFIX at the top of scripts/tilestreamingpartition.py. Set it to "" to merge all objects regardless of name.
After exporting assets, use Optimizations for optional workflows such as ASTC texture compression and LZ4 geometry compression.
Single asset:
setEntityMeshAsync(
entityId: entityId,
filename: "robot",
withExtension: "untold"
)Tiled scene:
let sceneRoot = createEntity()
setEntityName(entityId: sceneRoot, name: "dungeon")
setEntityStreamScene(entityId: sceneRoot, manifest: "dungeon", withExtension: "json")The manifest should live next to the tile payload directory. Tile, HLOD, LOD, and shared-bucket payloads are resolved relative to the manifest file.
.untoldtile payloads participate in the current tiled streaming architecture, including tile-level load/unload, remote download + cache, per-tile LOD/HLOD, and large-tile OCC sub-mesh streaming when the runtime classifies a tile into the OOC path.- The Python files in
scripts/are implementation details. The recommended user entry points are the shell wrappers in the same folder.