Your own material
The engine's lit models cover most surfaces. When one of yours needs light added up differently, you write the material once in the material language and the build compiles it for every backend. This page is the whole path for an application, in the order you take it. The example is a common one: the sun's visibility baked into the occlusion map, which must darken the sunlight and leave the sky alone.
1. Wire the build #
flutter pub add dev:flutter3d_build:^1.0.0-rc.2
dart run flutter3d_build:init
init writes hook/build.dart, which runs buildAssets on every flutter run and flutter build, adds the dev_dependencies: line, the flutter: assets: lines and a .gitignore line for flutter3d_generated/. The asset pipeline has the details. A project that already ran it for its models has nothing to add here.
2. Put the source under assets_src/ #
assets_src/
materials/
sun_visibility.f3dmat
The build compiles every .f3dmat under assets_src/ into a shader bundle at the same relative path under flutter3d_generated/, .f3dshaders in place of .f3dmat: this one becomes flutter3d_generated/materials/sun_visibility.f3dshaders.
Any directory works except assets_src/shaders/. flutter3d_generated/shaders/ holds the engine's own bundle and the plugins' materials, and the build refuses a material that would be written among them, naming the file and asking you to move it.
3. Run init again for a new directory #
Flutter bundles the files directly in a directory named under flutter: assets: and none in its subdirectories, so every directory the build writes into needs its own line. init writes one for each directory that holds a source when it runs. Run it again after you add a directory under assets_src/; for this project it leaves:
flutter:
assets:
- flutter3d_generated/
- flutter3d_generated/materials/
- flutter3d_generated/shaders/
Without the materials/ line the build still compiles the material, the app ships without it, and step 4 throws a StateError naming the path it could not read.
4. Load it and bind it #
import 'package:flutter3d/flutter3d.dart';
import 'package:flutter3d_app/flutter3d_app.dart';
Future<void> addGround(
Flutter3dEngine engine,
DeviceMesh ground,
TextureHandle sunVisibility,
) async {
final materials = await HotSwap.instance.loadMaterial(
'assets_src/materials/sun_visibility.f3dmat',
device: engine.device,
);
engine.renderer.renderSteps.addMaterials(materials.library);
final material = materials.bind(
RenderMaterial(name: 'ground', occlusion: sunVisibility),
'SunVisibility',
);
engine.scene.add(MeshNode(ground, material));
}
HotSwap.loadMaterial takes the path you wrote the source at and reads the bundle the build made from it. It works in every build mode; in a debug build it also watches the bundle. renderSteps.addMaterials gives the renderer the compiled stages, once for each .f3dmat. HotMaterials.bind sets the material's lighting model to the one named in the source (material SunVisibility) and a default for every uniform the material has no value for yet. When the hook has compiled an edit, a hot reload refreshes the library, relinks the renderer and binds every material bound this way again, so the next frame draws the edit.
Call it from Flutter3dView's onCreated, or wherever you have the engine. Everything on the RenderMaterial itself (base colour, maps, occlusionStrength) is set as for any other material.
Which backend compiles it when #
| Backend | At build | At run time |
|---|---|---|
| Impeller | impellerc, from the Flutter SDK that runs the hook |
Nothing: impellerc does not ship with an app, so an edit reaches the device only through a build |
| WebGL2 | GLSL ES 3.00 text | The browser compiles the text when the renderer first asks for the stage |
| WebGPU | WGSL, through glslangValidator and naga on the build machine |
The browser compiles the WGSL |
| Software | Nothing: the bundle carries the source | The device evaluates the source in Dart |
The build is the only step on Impeller, which is why the material has to go through assets_src/ and the hook rather than a script of your own. A mistake in the source fails the build with the file, line and column; GLSL a compiler refuses fails it with the compiler's message and keeps the generated .frag where the message says.
The WebGPU section is the one the build may leave out: without glslangValidator and naga on the build machine the bundle has no WGSL, and the WebGPU backend refuses it by name rather than drawing without the material. Install both before building for the web.
The example: sun visibility in the occlusion map #
By default the built-in models multiply the lights and the ambient alike by the occlusion map. That is right for ambient occlusion and wrong for a map that holds how much of the sun reaches each texel: a wall in the baked shadow of a roof goes as dark under the open sky as it does in the sun's direction. For the built-in lit models, occlusionAffects: OcclusionAffects.direct on the RenderMaterial does what the material below does, with no material of your own.
The material language lets you add up the light yourself:
f3dmat 2
material SunVisibility {
light { return albedo; }
composite { return direct * occlusion + indirect + emissive; }
fragment { return vec4(lit, alpha); }
}
light { return albedo; }is how the surface answers one light, the same as the engine's Lambert model. The engine multiplies it by the light's radiance, n·l and shadow. A material with alightblock is a lit model, so it also gets the normal, occlusion and emissive maps, the shadows and the light list.compositereplaces the sum. Heredirect(the lights) is multiplied by the occlusion andindirect(the albedo times ambient and lightmap) is not. Without the block,litis(direct + indirect) * occlusion + emissive.fragmentwrites the result with the surface's opacity.
occlusion is the red channel of RenderMaterial.occlusion, taken in full at occlusionStrength 1 and not at all at 0, as for the built-in models. Bake the sun's visibility into that channel and texels in its shadow keep only the sky. The light's own castsShadow still applies to direct: switch it off for surfaces whose sun is baked, or keep it for the moving objects the bake does not know about.
This material is a fixture of the engine's tests (flutter3d_build_hooks/test/fixtures/sun_visibility.f3dmat). The test compiles it with impellerc, glslangValidator and naga, checks that occlusion darkens the lights and not the sky on the software backend, and fails if this page shows anything else.
Next #
- The material language: every block, the uniforms and textures a material declares, and what each backend does with each construct.
- The asset pipeline: what else the hook builds, and what to do when it fails.
- Registered lighting models: a material for a plugin, which the plugin's users draw by name.