.untold is the native runtime asset container for UntoldEngine tile streaming.
It is not an interchange format and it is not intended to preserve full USD
semantics. USD/USDZ remains the authoring/import format. The .untold container is the
runtime package consumed by the engine for:
- tile files
- per-tile LOD files
- HLOD files
- shared bucket files
V1 is intentionally narrow:
- static meshes only
- transforms
- bounds
- vertex/index buffers
- basic PBR materials
- texture references
- no animation
- no skinning
- no blend shapes
The design goals are:
- fast runtime parse with no ModelIO dependency
- direct tile/HLOD/LOD streaming
- byte-range-friendly remote streaming (tiles are downloaded on demand from HTTP/HTTPS CDNs and cached locally before parsing; see
asset_remote_streaming.md) - explicit binary versioning
- stable on-disk layout independent of Swift ABI
- Endianness: little-endian for all integers and floats
- Float format: IEEE-754
Float32 - Chunk alignment: 16-byte aligned file offsets
- String encoding: UTF-8, null-terminated
- Invalid reference sentinel:
UInt32.max - Matrix encoding: 16
Float32s, column-major, matchingsimd_float4x4 - AABB encoding:
min.xyzfollowed bymax.xyz - File offsets: absolute
UInt64offsets from the start of the file - Chunk-local offsets:
UInt64offsets from the start of the uncompressed chunk payload - Versioning: unsupported versions must be rejected by the loader
The Swift types in
Sources/UntoldEngine/AssetFormat/UntoldFormat.swift
are the logical schema. They are not the authoritative on-disk memory layout.
The exporter and loader must write/read fields explicitly.
Each .untold file is laid out as:
FileHeaderChunkTable- aligned chunk payloads
Recommended chunk payload order:
STRING_TABLEENTITY_TABLEMESH_TABLEMATERIAL_TABLETEXTURE_TABLEVERTEX_DATAINDEX_DATA
This order allows the runtime to read metadata first and defer heavy geometry reads.
The header is written field-by-field in this exact order:
magic[8] UInt8 x 8
formatVersion UInt32
fileType UInt32
flags UInt32
headerSize UInt32
chunkCount UInt32
meshCount UInt32
materialCount UInt32
textureRefCount UInt32
entityCount UInt32
vertexLayout UInt32
reserved0 UInt32
worldBounds.min Float32 x 3
worldBounds.max Float32 x 3
rootTransform Float32 x 16
contentHash UInt8 x 32
reserved1 UInt8 x 32
Rules:
magicmust be exactly 8 bytes, recommended value:"UNTOLD\0\0"headerSizeis the serialized byte size of the header, notMemoryLayoutcontentHashis exactly 32 bytesreserved1is exactly 32 bytes
Each chunk entry is written in this exact order:
chunkType UInt32
compressionType UInt32
fileOffset UInt64
compressedSize UInt64
uncompressedSize UInt64
elementCount UInt32
reserved0 UInt32
Rules:
fileOffsetmust be 16-byte aligned- if compression is
none,compressedSize == uncompressedSize elementCountis used for record-table chunks
The string table payload is a raw byte blob containing:
- concatenated UTF-8 strings
- one
0x00terminator after each string
Rules:
- all string references are
UInt32byte offsets into this chunk UInt32.maxmeans “no string”- the exporter should deduplicate strings
- the loader must verify that each referenced offset is within the chunk and reaches a null terminator before chunk end
Each entity record is serialized in this exact order:
entityId UInt32
parentEntityId UInt32
nameOffset UInt32
firstMeshRecordIndex UInt32
meshRecordCount UInt32
flags UInt32
localBounds.min Float32 x 3
localBounds.max Float32 x 3
worldBounds.min Float32 x 3
worldBounds.max Float32 x 3
localTransform Float32 x 16
Rules:
parentEntityId == UInt32.maxmeans no parent- mesh records for one entity should be contiguous in the mesh table
firstMeshRecordIndex + meshRecordCountmust stay within mesh table bounds
Each mesh record is serialized in this exact order:
entityId UInt32
meshNameOffset UInt32
materialIndex UInt32
indexType UInt32
vertexCount UInt32
indexCount UInt32
vertexStrideBytes UInt32
flags UInt32
vertexDataOffset UInt64
indexDataOffset UInt64
vertexDataSizeBytes UInt64
indexDataSizeBytes UInt64
estimatedGPUBytes UInt64
reserved0 UInt64
localBounds.min Float32 x 3
localBounds.max Float32 x 3
Rules:
vertexDataOffsetis relative to the start ofVERTEX_DATAindexDataOffsetis relative to the start ofINDEX_DATAvertexDataSizeBytes == vertexCount * vertexStrideBytesindexDataSizeBytes == indexCount * indexElementSizematerialIndex == UInt32.maxmeans no material
Each material record is serialized in this exact order:
nameOffset UInt32
flags UInt32
baseColorFactor Float32 x 4
emissiveFactor Float32 x 3
normalScale Float32
metallicFactor Float32
roughnessFactor Float32
occlusionStrength Float32
alphaCutoff Float32
baseColorTextureIndex UInt32
normalTextureIndex UInt32
metallicTextureIndex UInt32
roughnessTextureIndex UInt32
emissiveTextureIndex UInt32
occlusionTextureIndex UInt32
reserved0 UInt32 x 2
Rules:
- texture indices point into
TEXTURE_TABLE - any texture index may be
UInt32.max flagsholds alpha mode, double-sided, transparent, and similar runtime bits
Each texture record is serialized in this exact order:
nameOffset UInt32
uriOffset UInt32
textureFormat UInt32
flags UInt32
width UInt32
height UInt32
mipCount UInt32
reserved0 UInt32
Rules:
uriOffsetpoints into the string table- the URI should reference a cooked texture asset or runtime-resolvable texture path
textureFormatdescribes the cooked/runtime texture format
V1 supports one layout only:
UNT_VERTEX_LAYOUT_PBR_STATIC_V1
Serialized layout:
position.x Float32
position.y Float32
position.z Float32
normalPacked UInt32
tangentPacked UInt32
uv0.u UInt16
uv0.v UInt16
uv1.u UInt16
uv1.v UInt16
color0.r UInt8
color0.g UInt8
color0.b UInt8
color0.a UInt8
Total size: 32 bytes
Rules:
uv0is requireduv1may be zeroed if unusedcolor0defaults to255,255,255,255if unused
normalPacked and tangentPacked use signed normalized 10:10:10:2 packing.
For normalPacked:
- X: signed normalized 10-bit
- Y: signed normalized 10-bit
- Z: signed normalized 10-bit
- W: unused, stored as 0
For tangentPacked:
- X: signed normalized 10-bit tangent x
- Y: signed normalized 10-bit tangent y
- Z: signed normalized 10-bit tangent z
- W: handedness sign encoded in signed normalized 2-bit space
Recommended tangent handedness mapping:
+1for non-negative handedness-1for negative handedness
Exporter rules:
- normalize input normal/tangent before packing
- clamp components to
[-1, 1] - write
normalPacked.w = 0 - write
tangentPacked.w = +1or-1
Runtime rule:
- reconstruct bitangent as
cross(normal, tangent.xyz) * tangentSign
indexType = uint16means 2 bytes per indexindexType = uint32means 4 bytes per index- all indices in one mesh must use one type
- exporter should prefer
uint16whenvertexCount <= 65535
Supported compression types:
nonelz4zstd
Rules:
- compression is applied per chunk, not whole-file
- offsets stored in metadata reference the uncompressed chunk payload layout
- metadata chunks may remain uncompressed for simpler startup
- geometry chunks may be compressed
The loader must reject files when:
- magic is invalid
- version is unsupported
- required chunks are missing
- chunk offsets exceed file length
- chunk offsets are not 16-byte aligned
- string offsets fall outside the string table
- mesh/entity/material/texture indices are out of range
- vertex or index ranges exceed their chunk bounds
vertexStrideBytesdoes not match the declared vertex layoutindexDataSizeBytesdoes not matchindexCount * indexElementSize
Do not serialize .untold files using MemoryLayout<T> or direct struct dumps.
Both the exporter and the loader must use explicit field-by-field helpers:
writeUInt32LEwriteUInt64LEwriteFloat32LEwriteBytesreadUInt32LEreadUInt64LEreadFloat32LEreadBytes
This keeps the binary stable even if the Swift type layout changes.