USDZ export
Open the live demo · Read the source · View on GitHub
.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');
}