.fmat material files
Open the live demo · Read the source · View on GitHub
A .fmat is a material as a file of its own, separate from any model that
wears it. That is what lets a studio's brushed steel be edited once and
shared by every mesh that uses it, instead of being re-exported into each
one of them by hand. readFmat and writeFmat are the two ends of it, and
what one writes, the other reads back equal.
Step 1: A material #
Metallic, rough, and tinted a cool grey. Nothing exotic, which is the point:
most of what a .fmat carries is exactly this handful of scalars.
final MaterialDocument document = MaterialDocument(
surface: SurfaceMaterial(
name: 'hull-plate',
baseColor: Vector4(0.55, 0.6, 0.65, 1.0),
metallic: 0.9,
roughness: 0.28,
),
);
Step 2: Write it, and read it back #
writeFmat turns the document into the JSON text a person would edit by
hand. readFmat turns that text back into a document, and the two are
supposed to agree on every field.
_text = writeFmat(document);
_roundTripped = readFmat(
Uint8List.fromList(utf8.encode(_text)),
name: 'hull-plate.fmat',
);
Step 3: A file with a typo in it #
An unknown key does not fail the whole file. roughnesss, misspelled, is
recorded as a warning and every field this reader does understand still
loads with it.
// An unknown key does not fail the file: it is recorded as a warning and
// everything this reader does know still loads.
const String typo =
'{"fmat": 1, "name": "typo", "roughnesss": 0.2, "baseColor": '
'[1, 1, 1, 1]}';
_typoWarnings = readFmat(
Uint8List.fromList(utf8.encode(typo)),
name: 'typo.fmat',
).warnings;
Step 4: The report, beside the material it describes #
The page shows the written text and both warning lists directly: a material
file is something to read first, not something to spin around first. But
baseColor, metallic and roughness are values a reader has intuitions
about, and the surest way to check those intuitions against what the file
actually says is to look at the surface those numbers shade — so a sphere
wearing the round-tripped material sits beside the report, and drags like
this app's ordinary viewport does.
String _report() =>
'$_text\n'
'round trip warnings: ${_roundTripped.warnings.isEmpty ? "none" : _roundTripped.warnings}\n'
'a file with an unknown key warns: $_typoWarnings';
Step 5: What this page checks #
The round trip has to leave metallic and roughness exactly where they
started, a clean file has to produce no warnings, and the misspelled file
has to produce at least one.
final SurfaceMaterial original = SurfaceMaterial(
metallic: 0.9,
roughness: 0.28,
);
final SurfaceMaterial read = _roundTripped.surface;
if (read.metallic != original.metallic ||
read.roughness != original.roughness) {
throw StateError('the round trip changed the material');
}
if (_roundTripped.warnings.isNotEmpty) {
throw StateError(
'a clean file should not warn: ${_roundTripped.warnings}',
);
}
if (_typoWarnings.isEmpty) {
throw StateError('an unknown key should have warned and did not');
}
if (frame.drawCalls < 1) {
throw StateError('the plate was not drawn');
}