read static method
The model in the cooked file bytes, named name for messages.
Implementation
static ModelData read(Uint8List bytes, {String? name}) {
if (!isCooked(bytes)) {
throw FormatException('${name ?? 'This file'} is not a cooked model.');
}
final view = ByteData.sublistView(bytes);
final found = view.getUint32(4, Endian.little);
if (found > version) {
throw FormatException(
'${name ?? 'This model'} was cooked by a newer version (format $found; this reads $version). Cook it again.',
);
}
int padded(int n) => (n + 3) & ~3;
final jsonLength = view.getUint32(8, Endian.little);
final header =
jsonDecode(
utf8.decode(Uint8List.sublistView(bytes, 12, 12 + jsonLength)),
)
as Map<String, dynamic>;
var at = 12 + padded(jsonLength);
final count = view.getUint32(at, Endian.little);
at += 4;
final blobs = <Uint8List>[];
for (var i = 0; i < count; i++) {
final n = view.getUint32(at, Endian.little);
blobs.add(Uint8List.sublistView(bytes, at + 4, at + 4 + n));
at += 4 + padded(n);
}
// Typed views need their start aligned; a copy when it isn't.
Uint8List aligned(int index) {
final b = blobs[index];
return b.offsetInBytes % 4 == 0 ? b : Uint8List.fromList(b);
}
Float32List floats(int index) {
final b = aligned(index);
return b.buffer.asFloat32List(b.offsetInBytes, b.lengthInBytes ~/ 4);
}
Uint32List uints(int index) {
final b = aligned(index);
return b.buffer.asUint32List(b.offsetInBytes, b.lengthInBytes ~/ 4);
}
List<double> nums(Object? v) => [
for (final e in (v as List? ?? const [])) (e as num).toDouble(),
];
Aabb3 box(Object? v) {
final n = nums(v);
return n.length == 6
? Aabb3.minMax(Vector3(n[0], n[1], n[2]), Vector3(n[3], n[4], n[5]))
: Aabb3();
}
TextureRef? ref(Object? v) {
if (v is! Map) return null;
final tx = nums(v['tx']);
return TextureRef(
v['t'] as int,
texCoord: v['uv'] as int? ?? 0,
offset: tx.length == 5 ? (tx[0], tx[1]) : (0.0, 0.0),
scale: tx.length == 5 ? (tx[2], tx[3]) : (1.0, 1.0),
rotation: tx.length == 5 ? tx[4] : 0.0,
);
}
List<int> ints(Object? v) => [
for (final e in (v as List? ?? const [])) e as int,
];
List<Map<String, dynamic>> maps(Object? v) =>
(v as List? ?? const []).cast<Map<String, dynamic>>();
return ModelData(
name: name ?? header['name'] as String? ?? 'model',
bounds: box(header['bounds']),
roots: ints(header['roots']),
warnings: [
for (final w in (header['warnings'] as List? ?? const [])) w as String,
],
meshes: [
for (final mesh in maps(header['meshes']))
MeshData(mesh['name'] as String?, [
for (final p in maps(mesh['primitives']))
PrimitiveData(
vertices: floats(p['v'] as int),
indices: uints(p['i'] as int),
skin: p['s'] == null ? null : floats(p['s'] as int),
material: p['m'] as int?,
bounds: box(p['b']),
hasTangents: p['tan'] as bool? ?? false,
),
]),
],
materials: [
for (final m in maps(header['materials']))
() {
final bc = nums(m['bc']), em = nums(m['em']);
return MaterialData(
name: m['name'] as String?,
baseColor: (bc[0], bc[1], bc[2], bc[3]),
baseColorTexture: ref(m['bct']),
metallic: (m['mt'] as num).toDouble(),
roughness: (m['rg'] as num).toDouble(),
metallicRoughnessTexture: ref(m['mrt']),
normalTexture: ref(m['nt']),
normalScale: (m['ns'] as num).toDouble(),
occlusionTexture: ref(m['ot']),
occlusionStrength: (m['os'] as num).toDouble(),
emissive: (em[0], em[1], em[2]),
emissiveTexture: ref(m['et']),
emissiveStrength: (m['es'] as num).toDouble(),
alphaMode: AlphaMode.values.byName(m['am'] as String),
alphaCutoff: (m['ac'] as num).toDouble(),
doubleSided: m['ds'] as bool,
unlit: m['ul'] as bool,
);
}(),
],
textures: [
for (final t in maps(header['textures']))
() {
final s = ints(t['sampler']);
return TextureData(
t['image'] as int?,
SamplerData(
magFilter: s[0],
minFilter: s[1],
wrapS: s[2],
wrapT: s[3],
),
);
}(),
],
images: [
for (final image in maps(header['images']))
image['levels'] is List
? ImageData(
Uint8List(0),
image['mime'] as String?,
image['name'] as String?,
levels: [
for (final l in image['levels'] as List)
RgbaImage(l[0] as int, l[1] as int, blobs[l[2] as int]),
],
levelsSrgb: image['srgb'] as bool? ?? true,
)
: ImageData(
blobs[image['bytes'] as int],
image['mime'] as String?,
image['name'] as String?,
),
],
nodes: [
for (final n in maps(header['nodes']))
() {
final t = nums(n['t']), r = nums(n['r']), s = nums(n['s']);
return NodeData(
name: n['name'] as String?,
local: Matrix4.fromList(nums(n['local'])),
children: ints(n['children']),
mesh: n['mesh'] as int?,
skin: n['skin'] as int?,
light: n['light'] as int?,
translation: t.length == 3 ? Vector3(t[0], t[1], t[2]) : null,
rotation: r.length == 4
? Quaternion(r[0], r[1], r[2], r[3])
: null,
scale: s.length == 3 ? Vector3(s[0], s[1], s[2]) : null,
);
}(),
],
lights: [
for (final l in maps(header['lights']))
() {
final c = nums(l['color']);
return LightData(
type: LightType.values.byName(l['type'] as String),
name: l['name'] as String?,
color: (c[0], c[1], c[2]),
intensity: (l['intensity'] as num).toDouble(),
range: (l['range'] as num?)?.toDouble(),
innerConeAngle: (l['inner'] as num).toDouble(),
outerConeAngle: (l['outer'] as num).toDouble(),
);
}(),
],
skins: [
for (final s in maps(header['skins']))
SkinData(
name: s['name'] as String?,
joints: ints(s['joints']),
inverseBindMatrices: floats(s['ibm'] as int),
skeleton: s['skeleton'] as int?,
),
],
animations: [
for (final a in maps(header['animations']))
AnimationData(
name: a['name'] as String,
duration: (a['duration'] as num).toDouble(),
channels: [
for (final c in a['channels'] as List)
AnimationChannelData(
sampler: c[0] as int,
node: c[1] as int,
path: AnimationPath.values.byName(c[2] as String),
),
],
samplers: [
for (final s in maps(a['samplers']))
AnimationSamplerData(
input: floats(s['in'] as int),
output: floats(s['out'] as int),
interpolation: AnimationInterpolation.values.byName(
s['interp'] as String,
),
width: s['w'] as int,
),
],
),
],
);
}