buildMaterials function

Future<void> buildMaterials({
  1. required BuildInput buildInput,
  2. required BuildOutputBuilder buildOutput,
  3. required List<String> materials,
  4. String bundleName = 'materials',
})

Compiles .fmat custom-material files into a Flutter GPU shader bundle plus a parameter-metadata sidecar, for use with ShaderMaterial / PreprocessedMaterial at runtime.

Call this from a consuming app's hook/build.dart, alongside buildModels and buildShaderBundleJson:

import 'package:hooks/hooks.dart';
import 'package:flutter_scene/build_hooks.dart';

void main(List<String> args) {
  build(args, (config, output) async {
    await buildMaterials(
      buildInput: config,
      buildOutput: output,
      materials: ['materials/toon.fmat'],
    );
  });
}

Each path in materials is resolved relative to the package root. The produced bundle is written to build/shaderbundles/[bundleName].shaderbundle (one fragment entry per material, named by the material's name), and the combined parameter sidecar to build/shaderbundles/[bundleName].fmat.json. List both as assets in the app's pubspec.

The generated shaders #include flutter_scene's framework GLSL; this hook puts flutter_scene's shaders/ directory on impellerc's include path (via buildShaderBundleJson's includeDirectories), so no framework files are copied into the consumer's project.

Implementation

Future<void> buildMaterials({
  required BuildInput buildInput,
  required BuildOutputBuilder buildOutput,
  required List<String> materials,
  String bundleName = 'materials',
}) async {
  final packageRoot = buildInput.packageRoot;

  // Locate flutter_scene's framework shader directory. flutter_scene has no
  // top-level `flutter_scene.dart` library, so resolve through this package's
  // `build_hooks.dart` (which always exists) and hop to the sibling `shaders/`.
  final frameworkLib = await Isolate.resolvePackageUri(
    Uri.parse('package:flutter_scene/build_hooks.dart'),
  );
  if (frameworkLib == null) {
    throw Exception(
      'buildMaterials could not resolve the flutter_scene package location.',
    );
  }
  final frameworkShaders = frameworkLib.resolve('../shaders/');

  // Generated GLSL and the synthesized manifest live under the package's build
  // directory; they are regenerated each run.
  final generatedDir = Directory.fromUri(
    packageRoot.resolve('build/fmat/$bundleName/'),
  );
  generatedDir.createSync(recursive: true);

  final manifest = <String, Object?>{};
  final sidecars = <String, Object?>{};
  final sourceDependencies = <Uri>[];

  for (final materialPath in materials) {
    if (!materialPath.endsWith('.fmat')) {
      throw Exception('Material files must end with ".fmat": $materialPath');
    }
    final materialUri = packageRoot.resolve(materialPath);
    final source = File(materialUri.toFilePath()).readAsStringSync();
    final compiled = compileFmat(source, fileName: materialPath);
    final entryName = compiled.material.name;

    if (manifest.containsKey(entryName)) {
      throw Exception(
        'Two materials in bundle "$bundleName" share the name "$entryName"; '
        'material names must be unique within a bundle.',
      );
    }

    final fragFileName = '$entryName.frag';
    File(
      generatedDir.uri.resolve(fragFileName).toFilePath(),
    ).writeAsStringSync(compiled.glsl);

    manifest[entryName] = <String, Object?>{
      'type': 'fragment',
      // impellerc resolves a bundle entry's `file` relative to the package
      // root (its working directory), so reference the generated shader from
      // there, not relative to the manifest.
      'file': 'build/fmat/$bundleName/$fragFileName',
    };
    sidecars[entryName] = compiled.sidecar;
    sourceDependencies.add(materialUri);
  }

  // Write the synthesized shader-bundle manifest next to the generated shaders,
  // so its `file` entries resolve relative to it.
  final manifestRelativePath =
      'build/fmat/$bundleName/$bundleName.shaderbundle.json';
  File(
    packageRoot.resolve(manifestRelativePath).toFilePath(),
  ).writeAsStringSync(const JsonEncoder.withIndent('  ').convert(manifest));

  // Compile, with flutter_scene's shaders/ on the include path so the generated
  // shaders' framework `#include`s resolve directly (no copies).
  await buildShaderBundleJson(
    buildInput: buildInput,
    buildOutput: buildOutput,
    manifestFileName: manifestRelativePath,
    includeDirectories: [frameworkShaders],
  );

  // Write the combined parameter sidecar next to the produced bundle.
  File(
    packageRoot
        .resolve('build/shaderbundles/$bundleName.fmat.json')
        .toFilePath(),
  ).writeAsStringSync(const JsonEncoder.withIndent('  ').convert(sidecars));

  // Declare the real inputs as dependencies. buildShaderBundleJson declares the
  // (generated) .frag files; the sources that actually drive a rebuild are the
  // .fmat files and the framework GLSL they include.
  buildOutput.dependencies.addAll(sourceDependencies);
  buildOutput.dependencies.addAll(
    _frameworkShaderFiles.map(frameworkShaders.resolve),
  );
}