flutter3d_build

The build hook behind a flutter3d project's assets. It converts model and texture sources into what the engine loads, and it does so on every build, so nobody has to remember to run a separate step.

dart pub add dev:flutter3d_build:^1.0.0-rc.2
dart run flutter3d_build:init          # hook/build.dart, pubspec, .gitignore
dart run flutter3d_build:init --check  # say what a run would change, change nothing

After that, anything under assets_src/ is converted into flutter3d_generated/ at the same relative path whenever the project builds, and a second build with nothing changed converts nothing. A flutter3d_assets.yaml beside the pubspec excludes files by glob.

The hook compresses textures for the platform it is building for: BC for a desktop target, ETC2 for Android and iOS. A block format is a property of the platform, so the hook does not have to guess about the device. A web build names no target and is left alone. Setting textures: under the project's own name in its hooks: user_defines: overrides either choice. The value universal there is the one family every device can load; it is turned into BC, ASTC, ETC2 or RGBA8 when the texture is uploaded.

The flutter3d command

dart pub global activate flutter3d_build 1.0.0-rc.2
flutter3d help

One command for every tool here: convert, create (create project <dir> with --kind scene, viewer or arcade, or create plugin), init, plugins, shaders, migrate, lights, lightmap, visibility, decode-worker and doctor. Each also runs as dart run flutter3d_build:<name> (decode_worker), the same code. flutter3d doctor checks Dart and Flutter against the versions in SUPPORT.md and looks for the optional programs: impellerc, glslangValidator and naga for building materials, and FBX2glTF, Blender and usdcat for conversion. Run in a project, it also reads the shader hook, the asset lines, the shader bundle and the keys that turn Flutter GPU on in each Info.plist and the Android manifest, and names the line to add for each one that is missing.

The command is a contract

From 1.0 the command is held to semver like the Dart API, because CI scripts and services call it and none of them compiles against this package. Within a major nothing below is removed or renamed, and the --json output only gains keys. api/flutter3d_build.cli is the whole surface as flutter3d help --surface prints it, and the repository's structure check fails when the two differ.

Subcommands: convert, create, init, plugins, shaders, migrate, lights, lightmap, visibility, decode-worker, doctor, help. Each also runs as dart run flutter3d_build:<name>, with decode_worker for decode-worker. flutter3d --version prints the package's version.

Flags, per subcommand. Every one also takes -h/--help, which prints its usage to stdout and exits 0; an option not listed exits 2.

Subcommand Flags
convert <input...> -o/--output <dir>, --dry-run, --overwrite, --asset-prefix <path>, --split, --no-materials, --report <file>, --json, --textures <family>, --no-mips, --lods <ratios>, --impostor, --chunks[=<triangles>]
create project <dir> [--kind <kind>] [--name <package>]; plugin --kind <kind> --name <name> [--target <dir>]; --list
init [dir] --check, --force
plugins --check, --json
shaders --full-shaders, --target native|web|any, --json
migrate <project> --dry-run, --data, --backup, --from <version>, --no-pub-get, --lints-from <dir>
lights --optimize <level.json>, --poses <file>, --state <file>, -o/--out <path>, --preview <dir>, --dry-run, --classes <names>
lightmap <level.json...> --density <n>, --bounces <n>, --samples <n>, --direct
visibility <level.json...> --cell <metres>
decode-worker [<output>] none
doctor --json
help [<command>] --surface

Exit codes, the same for every subcommand (CliExit in package:flutter3d_build/cli.dart):

Code Meaning
0 It did what it was asked, or there was nothing to do.
1 It tried and failed: a file, directory or pubspec that is not there or could not be read or written, an input that did not convert, a step that broke; under init --check, a hook/build.dart edited by hand.
2 Usage: an unknown subcommand or option, a missing argument.
3 Refused or out of date: --check with changes pending (init, plugins), a target directory that is not empty, an external tool convert needs that is not installed. Nothing was changed.
4 Would overwrite: convert found an output with other contents and was not given --overwrite. Nothing was written.

--json output is one JSON document on stdout that starts with the format envelope every flutter3d document starts with — "format": "f3d.cli", "version": 1, "requires" and "generator" — and "command", followed by the subcommand's own keys: convert gives output, dryRun and reports; doctor gives checks; plugins gives check, path, current or written, and plugins; shaders gives the bundle it wrote. flutter3d help --surface lists them, and api/flutter3d_build.cli commits that list; a test holds each command's printed keys to it, and a key gone since the last release is a break the structure check asks a **Breaking:** entry for, as an exit code or a flag is. version is the output's own number, not the package's. A key added is a minor bump of it, and nothing is removed or retyped within a major. convert --report <file> writes the same reports in the envelope f3d.convertReport.

flutter3d plugins --check writes nothing. It exits 3 when lib/plugins.g.dart is missing or differs from what the dependencies declare, which is the check a CI job wants after a dependency changes.

Bringing assets in

flutter3d convert Assets/Prefabs/Crate.prefab -o assets_src/imported
flutter3d convert 'scenes/**.tscn' model.usdz looks.mtlx -o assets_src/imported --dry-run

flutter3d convert reads glTF, GLB, OBJ with its MTL, STL, PLY, USDA and USDZ, MaterialX, Unity prefabs, scenes and materials, Godot scenes and materials, and FBX and .blend through FBX2glTF or Blender. Models become .f3d; materials become .fmat, or a .f3dmat program when a MaterialX graph computes an input; scenes and prefabs become level documents with prefabs, nested instances and their overrides included. Each input gets a report of what mapped and what was dropped and why (--json, --report). Each input becomes one .f3d: the model with its lights and cameras, its materials and textures, its programs and its scene as prefab documents, with the other models a scene names carried inside. --split writes them as separate files instead. Nothing is written over a file with other contents without --overwrite, and --dry-run writes nothing. Exit codes: 0 converted, 1 failed, 2 usage, 3 a needed tool is missing, 4 would overwrite.

The same conversion takes bytes and returns bytes for a server, with no external program and no Flutter:

import 'package:flutter3d_build/convert.dart';

final result = await convertFiles(uploadedFiles, to: ConversionTarget.everything);
// One bundle per input by default; bundle: false gives the split layout.

The format table and everything that does not survive the trip are on Bringing assets in.

dart run flutter3d_build:convert is flutter3d convert under its old name, as every file in bin/ is a flutter3d command. The one-model converter the build hook uses (one model or a directory of them, written next to the source or to a file -o names, one per device class with --classes) is runConvert, for a build script that wants it.

Materials

A material written in the material language goes under assets_src/ with the extension .f3dmat, next to the models that wear it. The hook compiles assets_src/fx/rim.f3dmat into flutter3d_generated/fx/rim.f3dshaders: one shader bundle with a fragment stage named after the material, carrying an Impeller section, a WebGL2 section and, when glslangValidator and naga are on PATH, a WebGPU section. Load it with GraphicsDevice.loadShaders. Compiling happens only here, at build time. A mistake fails the build with the material's file, line and column. A manifest rule's glob and exclude apply to materials as well.

A material may open with a line naming the language version it is written in, f3dmat 1, ahead of material. A file without that line is version 1, which covers every material written before the line existed. A build reads every version up to its own materialLanguageVersion. A newer file fails the build with the version that reads it.

Version 2 (f3dmat 2, 1.0) adds the state, vertex, ambient and composite blocks, the scene behind a translucent surface (sceneDepth, viewDepth, scenePosition), and two more kinds of source, fullscreen and compute. A material with a vertex block gets two more stages in its bundle, <Name>Vertex and <Name>VertexSkinned. A fullscreen source is built like a material. A compute source fails the build with the reason: no GPU backend loads a compute stage from a bundle. The whole language, with what each backend does with each construct, is on the site's material language page.

The build also writes lib/materials.g.dart, with a typed accessor for each material that declares a uniform: SeabedParams(material.parameters).time = t instead of material.parameters['time']![0] = t. That happens only in a package whose pubspec names vector_math, and the file is written only when its text changes. The string map stays as it was. It is what the renderer binds, and what a data plugin writes, since it has no Dart to generate.

The command converts the same way by hand:

flutter3d convert assets_src/chair.glb --textures universal

Steps of your own

The hook runs three built-in steps in order: models, materials and plugins. A BuildStep of your own (an importer, a baker) names the files it reads and writes, so the hook reruns when one changes, and places itself with after/before:

// `WindBaker` is a `BuildStep` of the plugin's own.
void main(List<String> arguments) async {
  await build(arguments, buildAssetsWith(<BuildStep>[WindBaker()]));
}

A plugin package exports its steps and the application names them here. buildAssets is buildAssetsWith(const []). A step that throws fails the build with its name.

Writing a plugin

flutter3d create plugin --kind render-step --name my_glow
flutter3d create --list

(or dart run flutter3d_build:create …, the same command) makes a new package with the plugin, its flutter3d_plugins: marker for discovery, a test that installs it, and test/conformance_test.dart, which runs flutter3d_conformance's plugin suite. The six kinds are render-step, effect, genre, element, tool and backend, the last a graphics backend started from the null backend rather than a plugin. The directory must be empty or not exist.

dart run flutter3d_build:plugin_mcp is the same for an agent: an MCP server with plugin.create, plugin.conformance (runs the suite and reads it test by test) and plugin.report (each check, and whether the plugin earns the conformance badge). Its tools are snapshotted in api/flutter3d_build.mcp.

skills/ holds one skill, flutter3d-build-authoring-a-plugin: which kind to choose, what each check means and what the badge needs. A project depending on this package installs it with dart run skills@ get.

Plain Dart

The Flutter tool starts hook/build.dart as a separate process, with no window and no Flutter SDK to resolve inside it. None of this package's dependencies (flutter3d_core, package:hooks, package:code_assets and a few plain-Dart libraries) names Flutter, because a hook that needed the SDK would not start on a machine that does not have it.

Libraries

cli
The flutter3d command line, as functions: runFlutter3d, the whole command, and the parts a test or a tool calls on its own (convert, doctor, create project). Every file in bin/ is one line over runFlutter3d: bin/flutter3d.dart the command, the others its subcommands under their dart run flutter3d_build:<name> names.
convert
Conversion as a library: convertFiles takes the bytes of an upload or of an unpacked Unity, Godot or USD bundle and returns the engine's files — .f3d, .fmat, .f3dmat, level documents with prefabs — with a report of what mapped, what was approximated and what was dropped.
flutter3d_build
ap-02's scaffold, filled in by ap-03: dart run flutter3d_build:convert, the same converter packages/flutter3d/tool/convert_asset.dart was, moved here so a build hook (ap-05) and a project that only has the published flutter3d package can both reach it.
lightmap
Baking a level's lightmap: LightmapBaker, what dart run flutter3d_build:lightmap runs over each level before it ships.