USDZ export: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: write (line 27), unzip (line 32), report (line 56), check (line 82).
1/// Writing geometry as `.usdz`, and reading the text layer back out of its
2/// own ZIP container.
3///
4/// Quoted by `usdz.md` and shown whole in the Source tab.
5library;
7import 'dart:convert';
8import 'dart:typed_data';
10import 'package:flutter/widgets.dart';
11import 'package:flutter3d/flutter3d.dart';
12import 'package:flutter3d_showcase/src/demo/demo.dart';
13import 'package:vector_math/vector_math.dart';
15final class UsdzDemo extends ShowcaseDemo {
16 late final Uint8List _archive;
17 late final String _usda;
19 final _document = PlainModelDocument(
20 surfaces: <ModelSurface>[
21 ModelSurface(mesh: CuboidShape(size: Vector3.all(1.0)).build()),
22 ],
23 );
26 Scene build(DemoContext context) {
27 // Geometry only: no materials, no hierarchy past one `Mesh` prim a
28 // surface. A `.usdz` is a ZIP with its entries stored uncompressed and
29 // aligned, so what Quick Look opens is the same bytes this writes.
30 _archive = UsdzWriter(_document, name: 'cube').write();
32 // Nothing in this package reads `.usdz` back, but the archive is a
33 // plain ZIP with one uncompressed entry, so its text layer can be read
34 // straight off the local file header this writer wrote first.
35 final view = ByteData.sublistView(_archive);
36 final nameLength = view.getUint16(26, Endian.little);
37 final extraLength = view.getUint16(28, Endian.little);
38 final dataLength = view.getUint32(22, Endian.little);
39 final dataStart = 30 + nameLength + extraLength;
40 _usda = utf8.decode(_archive.sublist(dataStart, dataStart + dataLength));
42 return Scene()
43 ..add(
44 MeshNode(
45 DeviceMesh.upload(context.device, _document.surfaces.single.mesh),
46 Material(baseColor: Vector4(0.85, 0.82, 0.7, 1.0)),
47 name: 'cube',
48 ),
49 )
50 ..add(
51 LightNode(name: 'sun', intensity: 3.0)
52 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
53 );
54 }
56 String _report() =>
57 'archive bytes: ${_archive.length}\n'
58 'first four bytes: '
59 '${String.fromCharCodes(_archive.take(2))}'
60 '${_archive[2]}${_archive[3]}\n'
61 '.usda bytes: ${_usda.length}\n'
62 'first line: ${_usda.split('\n').first}';
65 Widget? customBody(BuildContext buildContext, DemoContext context) =>
66 Container(
67 color: const Color(0xFF14161A),
68 padding: const EdgeInsets.all(24),
69 alignment: Alignment.topLeft,
70 child: DefaultTextStyle(
71 style: const TextStyle(
72 color: Color(0xFFE8E8EC),
73 fontSize: 16,
74 fontFamily: 'monospace',
75 ),
76 child: Text(_report()),
77 ),
78 );
81 void verify(Scene scene, FrameResult frame) {
82 if (_archive[0] != 0x50 || _archive[1] != 0x4b) {
83 throw StateError('the archive does not start with the ZIP signature');
84 }
85 if (!_usda.startsWith('#usda 1.0')) {
86 throw StateError('the text layer is not a USD ASCII file');
87 }
88 if (!_usda.contains('def Mesh')) {
89 throw StateError('the text layer has no mesh prim');
90 }
91 if (frame.drawCalls < 1) {
92 throw StateError('the cube was not drawn');
93 }
94 }
95}