flutter3d
Showcase Changelog 38 packages API reference

USDZ export

since 0.7.0 Formats and I/O

.usdz is the format Quick Look opens on macOS and iOS: a ZIP archive with a .usda text layer inside it. This writer is a spike, geometry only, and this page writes one, then reads its own text layer back out.

Step 1: Write the archive #

UsdzWriter bakes each surface's transform into its positions, the same way the STL and OBJ writers do, and writes one Mesh prim a surface. The ZIP itself stores every entry uncompressed and aligned to 64 bytes, which is what lets a real USD reader memory-map a layer straight out of the file instead of inflating it first.

// Geometry only: no materials, no hierarchy past one `Mesh` prim a
// surface. A `.usdz` is a ZIP with its entries stored uncompressed and
// aligned, so what Quick Look opens is the same bytes this writes.
_archive = UsdzWriter(_document, name: 'cube').write();

Note. No materials and no hierarchy past one flat set of meshes: fmt-27's own acceptance line asks for geometry, and growing this writer past that is a question for whoever first needs textured .usdz.

Step 2: Read the text layer back #

Nothing in this engine reads a .usdz, but the archive this writer produces is a plain ZIP with one stored entry, so the .usda text sits at a fixed offset past the local file header this writer wrote first.

// Nothing in this package reads `.usdz` back, but the archive is a
// plain ZIP with one uncompressed entry, so its text layer can be read
// straight off the local file header this writer wrote first.
final view = ByteData.sublistView(_archive);
final nameLength = view.getUint16(26, Endian.little);
final extraLength = view.getUint16(28, Endian.little);
final dataLength = view.getUint32(22, Endian.little);
final dataStart = 30 + nameLength + extraLength;
_usda = utf8.decode(_archive.sublist(dataStart, dataStart + dataLength));

Step 3: Look at both layers #

The ZIP signature at the front of the file, and the USD header at the front of the text it contains.

String _report() =>
    'archive bytes: ${_archive.length}\n'
    'first four bytes: '
    '${String.fromCharCodes(_archive.take(2))}'
    '${_archive[2]}${_archive[3]}\n'
    '.usda bytes: ${_usda.length}\n'
    'first line: ${_usda.split('\n').first}';

Step 4: Check the claim #

An archive that starts with the ZIP signature, a text layer that starts with #usda 1.0, and a Mesh prim somewhere inside it.

if (_archive[0] != 0x50 || _archive[1] != 0x4b) {
  throw StateError('the archive does not start with the ZIP signature');
}
if (!_usda.startsWith('#usda 1.0')) {
  throw StateError('the text layer is not a USD ASCII file');
}
if (!_usda.contains('def Mesh')) {
  throw StateError('the text layer has no mesh prim');
}
if (frame.drawCalls < 1) {
  throw StateError('the cube was not drawn');
}