# Custom materials in flutter_scene

flutter_scene gives you two ways to write a custom material:

1. **The `.fmat` declarative format (recommended).** You declare your
   parameters once and fill in a small `Surface()` function in GLSL. A build
   hook compiles it, and `PreprocessedMaterial` wires it up at runtime: typed,
   name-addressed parameters with no std140 packing by hand, and the engine's
   physically based lighting for free if you want it. This is the path most
   materials should use.
2. **`ShaderMaterial` (the low-level escape hatch).** You write complete raw
   GLSL, for the fragment stage and optionally the vertex stage too, declare
   your own uniform blocks and samplers, and bind them by name from Dart,
   packing std140 yourself. Use this when you need full control or a shader
   shape the `.fmat` format doesn't cover yet.

Both paths share the same engine contract (the vertex outputs your shader
receives and the color it must output), documented below. If you've used
Filament's `.mat` files or Godot's shaders, the `.fmat` model will feel
familiar; if you've used Three.js's `ShaderMaterial`, that's `ShaderMaterial`
here.

What is built and what is still missing is in [Current state and what's
next](#current-state-and-whats-next).

## Generated shader assets

Keep shader sources, `.fmat` files, and `.shaderbundle.json` source manifests in
version control. A `.shaderbundle.json` file only names shader entry points and
their source files. It is portable input to the build hook.

The generated `.shaderbundle` is different. It is compiled by the `impellerc`
that ships with the active Flutter engine and must be rebuilt by that same
toolchain. Register it as a DataAsset from `hook/build.dart`. Do not commit it,
copy it into an assets directory, or list it under `flutter.assets` in
`pubspec.yaml`.

---

# The `.fmat` format

## Quick start

Author a material. A `.fmat` file has two blocks: a `material { }` metadata
block and a `fragment { }` GLSL block.

```
// assets/toon.fmat
material {
  name: "Toon",
  shading_model: unlit,
  blending: opaque,
  culling: back,

  parameters: [
    { type: vec4,      name: base_color, hint: source_color, default: [1, 1, 1, 1] },
    { type: vec3,      name: light_direction, default: [0.4, 0.7, 0.5] },
    { type: int,       name: band_count, hint: range(1, 8, 1), default: 3 },
    { type: sampler2d, name: base_color_texture, hint: default_white },
  ],
}

fragment {
  void Surface(inout MaterialInputs material) {
    vec3 n = GetWorldNormal();
    float n_dot_l = max(dot(n, normalize(material_params.light_direction)), 0.0);
    float bands = max(float(material_params.band_count), 1.0);
    float banded = floor(n_dot_l * bands) / bands;

    vec4 tex = texture(base_color_texture, GetUV0());
    material.base_color = vec4(
        material_params.base_color.rgb * tex.rgb * banded,
        material_params.base_color.a * tex.a);
    PrepareMaterial(material);
  }
}
```

Install the DataAssets build hook once from your app root.

```sh
dart run flutter_scene:init
flutter config --enable-dart-data-assets
```

The generated hook auto-discovers `assets/**/*.fmat`, compiles the materials,
and registers the generated `.shaderbundle`, `.fmat.json` sidecar, and runtime
index as DataAssets. This path requires a Flutter toolchain with Dart DataAssets
support; while the feature is experimental, that means a supported Flutter
master build with `enable-dart-data-assets` enabled.

Then load the material by its source path (relative to the package root, so
two materials that share a `name` in different directories do not collide):

```dart
import 'package:flutter_scene/scene.dart';

final toon = await loadFmatMaterial('assets/toon.fmat');
toon.parameters
  ..setColor('base_color', const Color(0xFFE0A030))
  ..setInt('band_count', 4)
  ..setTexture('base_color_texture', myTexture);

node.mesh!.primitives[0].material = toon;
```

No generated files belong in `flutter.assets`. Materials loaded this way **hot
reload**. Render the scene with a `SceneView` and editing `assets/toon.fmat`
updates the running app in place
(see [Hot reload](#hot-reload)).

`MaterialAssetMode.legacyOnly` remains available only for older Flutter
toolchains without DataAssets. It requires manual `flutter.assets` entries and
does not provide the managed source-path loading and hot-reload workflow.

The bundle entry name and the sidecar key are the material's `name`
(`"Toon"` above). One `buildMaterials` call can compile several `.fmat` files
into one bundle; each becomes an entry keyed by its `name`.

## The `material` block

| Key | Values | Default | Meaning |
| --- | --- | --- | --- |
| `name` | string (required) | | The bundle entry name and sidecar key. |
| `shading_model` | `lit`, `unlit` | `lit` | `lit` runs the engine's PBR lighting; `unlit` outputs your color directly. |
| `blending` | `opaque`, `alpha` | `opaque` | `alpha` routes the material through the depth-sorted translucent pass. |
| `culling` | `back`, `front`, `none` | `back` | Which faces are culled; `none` is double-sided. |
| `parameters` | list of objects | `[]` | The material's parameters (see below). |
| `engine_inputs` | list of `scene_color`, `scene_depth` | `[]` | Per-frame engine textures the shader samples (see below). Lit surface materials only. |

## Engine inputs (`engine_inputs`)

A material can sample what the scene rendered behind it by declaring engine
inputs. The engine produces these only when a visible material asks, so they
cost nothing when unused:

- `scene_color` binds `scene_opaque_color`, a snapshot of the scene taken
  after the opaque phase (skybox + opaque draws) and before translucent
  draws. Requesting it splits the scene pass into two GPU passes with a
  resolve in between. Use it for refraction and translucent compositing.
- `scene_depth` binds `scene_depth`, the opaque geometry's linear
  (planar view-space) depth in world units. Requesting it forces the depth
  prepass (already produced when SSAO or reflections are on). Use it for
  depth-fade absorption, shoreline foam, and soft-particle edges.

Declaring an input emits the sampler and these accessors into your shader:

```glsl
vec2  GetScreenUv();                  // this fragment's screen UV
vec3  GetSceneColor(vec2 uv_offset);  // opaque scene color behind the fragment
float GetSceneDepth(vec2 uv_offset);  // opaque linear depth behind the fragment
float GetFragmentViewDepth();         // this fragment's own linear depth
float GetTime();                      // engine seconds, for animation
```

`GetFragmentViewDepth()` and `GetSceneDepth(vec2(0.0))` are directly
comparable: their difference is the world-space thickness between this
surface and whatever is behind it. When an input is unavailable for a frame
(for example a non-perspective camera produces no depth), the accessors
return inert values (black, a huge depth) so effects fade out instead of
misrendering.

Typical use, a translucent water surface:

```glsl
float thickness = GetSceneDepth(vec2(0.0)) - GetFragmentViewDepth();
float foam = 1.0 - smoothstep(0.0, foam_width, thickness);       // shoreline
vec3 refracted = GetSceneColor(normal_offset * refraction_strength);
vec3 absorbed = refracted * exp(-absorption * thickness);         // Beer-Lambert
```

Only `lit` surface materials may declare engine inputs (the gates and screen
mapping ride the engine lighting data). Budget note: the lit framework is
close to Metal's 16-sampler limit, so with both inputs declared keep the
material's own samplers to two or fewer; prefer in-shader procedural noise
over noise textures.

## Parameters

Each parameter is `{ type, name, hint?, default? }`.

**Types.** Scalar and vector types (`float`, `int`, `vec2`, `vec3`, `vec4`,
`mat4`) are packed into a uniform block named `MaterialParams`; you read them in
the shader as `material_params.<name>`. Sampler types (`sampler2d`,
`samplerCube`) are top-level uniforms; you read them by their bare name. `mat3`
is intentionally unsupported because of a std140 layout bug on the GLES backend;
use `mat4`.

**Hints** add editor and runtime semantics:

| Hint | Valid on | Effect |
| --- | --- | --- |
| `source_color` | `vec3`, `vec4` | The value is an sRGB-authored color; `setColor` decodes it to linear. |
| `range(min, max, step)` | `float`, `int` | A bounded numeric range (recorded for tooling). |
| `default_white` / `default_black` / `default_normal` / `default_transparent` | samplers | The placeholder texture used until you set one. |

**Defaults** are a number for scalars, or a list for vectors and matrices
(`default: [1, 1, 1, 1]` for a `vec4`). Samplers take a placeholder via their
hint, not a `default`. Defaults are applied when the material is constructed, so
an unset parameter still renders sensibly.

## The `fragment` block

The `fragment` block holds GLSL. A `lit` material must define
`void Surface(inout MaterialInputs material)`; you fill the surface description
and the engine runs the lighting. An `unlit` material's `Surface()` writes the
final color into `material.base_color`.

`MaterialInputs` is:

```glsl
struct MaterialInputs {
  vec4 base_color;   // linear rgb, straight (non-premultiplied) alpha
  vec3 normal;       // world-space shading normal
  vec3 emissive;     // linear emissive radiance (lit only)
  float metallic;    // 0 dielectric .. 1 conductor (lit only)
  float roughness;   // perceptual roughness, 0..1 (lit only)
  float occlusion;   // ambient occlusion, 1 = unoccluded (lit only)
};
```

Call `PrepareMaterial(material)` before returning from `Surface()` (a Filament
convention; it is reserved for derived-value setup).

**Engine inputs are read through accessors** rather than the raw varyings:

```glsl
vec3 GetWorldPosition();   // world-space fragment position
vec3 GetWorldNormal();     // normalized world-space geometric normal
vec3 GetViewDirection();   // normalized direction toward the camera
vec2 GetUV0();             // primary texture coordinates
vec4 GetVertexColor();     // interpolated per-vertex color (white if none)
```

The standard GLSL helpers from the engine's shader library are `#include`d for
you and available in `Surface()`: `SRGBToLinear`, the Cook-Torrance BRDF pieces
(`FresnelSchlick`, `DistributionGGX`, ...), `PerturbNormal` (normal-map
perturbation), and `SamplePrefilteredRadiance`.

For a `lit` material, fill `base_color` / `metallic` / `roughness` / `normal` /
`occlusion` / `emissive` and the engine produces the lit color (image-based
lighting plus the scene's directional light, with shadows). For an `unlit`
material, compute whatever you want and write it into `base_color`; the engine
outputs it premultiplied.

> The per-light `light()` hook (a custom BRDF inside the engine light loop) is
> not implemented yet; today, `lit` uses the engine BRDF and `unlit` gives you
> full control.

---

# The `vertex` block

A surface material may add an optional `vertex { }` block to customize the
vertex stage (displace geometry, animate it, perturb normals, feed data to the
fragment). You write one function:

```glsl
vertex {
  void Vertex(inout VertexInputs vertex) {
    // Read and modify the vertex, in place.
  }
}
```

You write it once. The engine runs it on every mesh type and pass (static,
skinned, and the position-only depth/shadow pass); you never branch on whether
the mesh is skinned. Skinning is already applied when `Vertex()` runs, so the
fields below mean the same thing everywhere.

`VertexInputs` is:

```glsl
struct VertexInputs {
  vec3 position;        // object space (post-skinning on a skinned mesh)
  vec3 normal;          // object space
  vec4 tangent;         // object-space direction and bitangent sign
  vec3 world_position;  // world space, after the model/skin transform
  vec3 world_normal;    // world space
  vec4 world_tangent;   // world-space direction and bitangent sign
  vec2 uv;
  vec2 uv1;
  vec4 color;
  vec3 camera_position; // read-only, world space
};
```

Write `world_position` to displace geometry (the engine projects it to clip
space after `Vertex()` returns) and `world_normal` to change the shading normal.
The `material_params.*` values are available in `Vertex()` just as in
`Surface()`, so one parameter drives both stages.

```glsl
// A world curve: bend geometry down with distance from the camera.
void Vertex(inout VertexInputs vertex) {
  vec3 rel = vertex.world_position - vertex.camera_position;
  vertex.world_position.y -= material_params.curvature * dot(rel.xz, rel.xz);
}
```

**Derive, don't replace.** Prefer perturbing the provided value
(`vertex.world_normal = normalize(vertex.world_normal + delta)`) over assigning
a fresh one. It keeps the mesh normal meaningful, and it reads the mesh input so
the input can't be optimized away. (The engine inserts a keep-alive so a full
replacement still compiles, but deriving is the better habit.)

## Custom varyings (vertex to fragment)

Declare named interpolants in a `varyings` list; `Vertex()` writes them and
`Surface()` reads them, by name. The emitter generates the matching `out`/`in`
declarations, so you never pick a location.

```
material {
  name: "Curve",
  varyings: [ { type: float, name: curve_fade } ],   // float/vec2/vec3/vec4
}
vertex {
  void Vertex(inout VertexInputs vertex) { /* ... */ curve_fade = ...; }
}
fragment {
  void Surface(inout MaterialInputs material) {
    material.base_color.rgb *= mix(1.0, 0.4, curve_fade);
    PrepareMaterial(material);
  }
}
```

## Custom vertex attributes (mesh to vertex)

Declare named per-vertex inputs in an `attributes` list; the mesh supplies the
data and `Vertex()` reads each by name.

```
material {
  name: "Waves",
  attributes: [ { type: float, name: phase } ],   // float/vec2/vec3/vec4
}
vertex {
  void Vertex(inout VertexInputs vertex) {
    vertex.world_position.y += 0.2 * sin(phase);
  }
}
```

Supply the data on the geometry, one value per vertex, matching by name:

```dart
geometry.setCustomAttribute('phase', phaseValues, components: 1);
```

Custom attributes require the described-layout (unskinned) geometry path
(`MeshGeometry` and the built-in primitives use it; skinned meshes do not
support custom attributes yet). The depth/shadow pass fetches only position, so
an attribute reads zero there: a displacement driven by a custom attribute is
not reflected in the shadow, while one driven by `world_position` / a parameter
is (world position is available in every pass).

---

# Built-in noise, `#include <noise.glsl>`

Any `fragment`, `vertex`, or `sky` block can opt into the engine's noise
library by starting the block with an include:

```glsl
fragment {
#include <noise.glsl>

  void Surface(inout MaterialInputs material) {
    float n = NoiseSimplex3(GetWorldPosition() * 4.0, 1337);
    ...
  }
}
```

The library is a GPU port of the FastNoiseLite implementation behind
`package:flutter_scene/noise.dart`, and the two are kept in lockstep, the
same field sampled on the CPU and evaluated in a shader agree. Functions
(each also in a smoother `2S`/`3S` OpenSimplex2S flavor):

```glsl
float NoiseSimplex2(vec2 p, int seed);   // one octave, roughly [-1, 1]
float NoiseSimplex3(vec3 p, int seed);
float NoisePerlin2(vec2 p, int seed);    // also NoisePerlin3
float NoiseValue2(vec2 p, int seed);     // also NoiseValue3
float NoiseCellular2(vec2 p, int seed, int distanceFunction, int returnType, float jitter);
float NoiseFbm2(vec2 p, int seed, int octaves, float lacunarity, float gain);
float NoiseRidged2(vec2 p, int seed, int octaves, float lacunarity, float gain);
float NoisePingPong2(vec2 p, int seed, int octaves, float lacunarity, float gain, float strength);
vec2  NoiseDomainWarp2(vec2 p, int seed, float amp);  // also NoiseDomainWarp3
vec3  NoiseCurl3(vec3 p, int seed, float epsilon);    // divergence-free, for advection
int   NoiseHash2(ivec2 cell, int seed);  // hashed lattice cell, full int32
```

Cellular distance functions and return types are int constants
(`kNoiseCellularEuclidean`/`EuclideanSq`/`Manhattan`/`Hybrid` and
`kNoiseCellularCellValue`/`Distance`/`Distance2`/`Distance2Add`/`Sub`/`Mul`/
`Div`) matching the Dart enums in declaration order. The GLSL domain warp
covers the default OpenSimplex2 type without warp fractals; the reduced and
basic-grid types and warp fractals are CPU-only for now.

Frequency is applied by the caller (scale `p` before the call), so
`FastNoiseLite(seed: s, frequency: f).getNoise2(x, y)` on the CPU corresponds
to `NoiseSimplex2(vec2(x, y) * f, s)` in the shader. The agreement contract
has two tiers. `NoiseHash2`/`NoiseHash3` are pure 32-bit integer math and
match the Dart `noiseHash2`/`noiseHash3` bit for bit on every backend, use
them for decisions that must never disagree. The float functions match the
CPU within a small tolerance (float32 rounding differs per GPU), enforced by
a per-backend parity test in CI; do not re-derive a hard threshold from float
noise on both sides, make the decision once and share it.

The GLSL noise is correct on every backend, including the web (WebGL2). The
Dart `FastNoiseLite` is currently correct only on native, its 32-bit integer
hash overflows on the web (where Dart `int` is a JavaScript double), so on the
web prefer the GLSL side or a `bakeNoiseTexture` built at build time or in a
native isolate. A web-safe Dart multiply is a planned follow-up.

`noise.glsl` carries the gradient and cell-vector lookup tables (the cellular
functions add about 1500 float constants), so a material that uses many noise
functions compiles to a large shader. That is fine on real GPU drivers, but a
software compiler (a device emulator, some headless CI) can run out of memory
on it; a material that uses one or two functions stays small. Bake to a
texture when a single field would do.

# The engine contract (both paths)

flutter_scene's engine vertex shaders (`UnskinnedVertex` and `SkinnedVertex`)
emit the same standard outputs. The `.fmat` accessors wrap these; a raw
`ShaderMaterial` declares them directly:

```glsl
in vec3 v_position;        // world space
in vec3 v_normal;          // world space, not necessarily unit length
in vec3 v_viewvector;      // camera_position - vertex_position
in vec2 v_texture_coords;
in vec2 v_texture_coords_1;
in vec4 v_color;           // per-vertex color, white when the model has none
in vec3 v_model_scale;
in vec4 v_tangent;         // world-space tangent and bitangent sign
```

The fragment output is `out vec4 frag_color;` at location 0.

**Output linear color premultiplied by alpha.** flutter_scene renders into a
floating-point HDR scene-color target and then runs one full-screen resolve pass
that applies exposure (`Scene.exposure`), the tone-mapping operator
(`Scene.toneMapping`, Khronos PBR Neutral by default), and the display EOTF. So
your shader outputs *linear* radiance (do not tone-map or gamma-encode), and
premultiplies rgb by alpha. Values above 1.0 are fine — the tone curve rolls
them off. When you sample an sRGB texture, linearize it first (`SRGBToLinear`,
or `pow(c, vec3(2.2))`). A `.fmat` material gets the premultiplied output for
free; `EvaluateLighting` (lit) and the unlit path both handle it.

The vertex `FrameInfo` block (model / camera matrices) is engine-bound and not
visible in the fragment stage; the world-space outputs already encode it.

---

# Building: the `buildMaterials` hook

`buildMaterials` (from `package:flutter_scene/build_hooks.dart`) preprocesses
each `.fmat`, emits GLSL, compiles it through `impellerc`, and generates these
managed outputs:

- `<bundleName>.shaderbundle` — the compiled Flutter GPU shader bundle.
- `<bundleName>.fmat.json` — the parameter sidecar the runtime needs.
- `<bundleName>.index.json` — the source-path index used by
  `loadFmatMaterial`.

`bundleName` defaults to `materials`. If `materials` is omitted,
`buildMaterials` discovers `assets/**/*.fmat` automatically; pass
`discoveryRoot` to search a directory other than `assets/`.

Use `MaterialAssetMode.dataAssetsRequired`, which fails early with setup
guidance when DataAssets are unavailable. This is what
`dart run flutter_scene:init` installs. `dataAssetsIfAvailable` and
`legacyOnly` exist for compatibility with older toolchains and should not be
used by new applications.

The generated shaders `#include` flutter_scene's framework GLSL; the hook puts
that directory on `impellerc`'s include path for you, so nothing is copied into
your project.

You can call `buildMaterials` alongside `buildScenes` and
`buildShaderBundleJson` in the same hook.

---

# Runtime: `PreprocessedMaterial` and `MaterialParameters`

Load each material by its checked-in source path with `loadFmatMaterial`, as in
the quick start. The registry resolves its generated bundle, sidecar, and entry
from the DataAsset index. Set its parameters through `material.parameters`, a
`MaterialParameters`.

`MaterialParameters` is type-checked and name-addressed. You never compute std140
offsets: parameter types come from the sidecar, byte offsets come from the
compiled shader's reflection, and a wrong-typed value throws instead of silently
corrupting the uniform block. Three tiers share one backing buffer:

```dart
// Typed setters (the safe default):
params.setFloat('rim_width', 0.2);
params.setVec4('tint', Vector4(0.5, 0.3, 1.0, 1.0));
params.setColor('base_color', const Color(0xFF8844FF)); // sRGB-decoded if source_color
params.setTexture('base_color_texture', myTexture);

// Dynamic, dispatches on the declared type and throws on a mismatch:
params['rim_width'] = 0.2;        // ok
params['rim_width'] = Vector4.zero(); // throws: rim_width is float

// Raw escape hatch for hot loops (you own correctness here):
params.rawBlock.setFloat32(params.offsetOf('rim_width'), 0.2, Endian.host);
```

A `source_color` parameter is sRGB-decoded to linear on `setColor` (matching the
shader's `SRGBToLinear`), so authored colors look right. Setting an unknown name
or a wrong type throws an `ArgumentError` with a message naming the parameter and
its declared type.

For a `lit` material, set `PreprocessedMaterial.environment` to override the
scene-wide image-based-lighting environment for that material.

---

# `ShaderMaterial`: the low-level escape hatch

When you need a shader shape the `.fmat` format doesn't cover, write a complete
raw fragment shader and drive it with `ShaderMaterial`. You declare your own
uniform blocks and samplers and bind them by name, packing std140 yourself.

`ShaderMaterial` covers both stages. Pass only a `fragmentShader` and the
engine's vertex shader runs, which is the common case. Pass a `vertexShader`
too and the material owns the whole pipeline; see [Raw vertex
shaders](#raw-vertex-shaders) for the contract yours has to meet.

```glsl
// shaders/vertex_color.frag
uniform FragInfo { vec4 tint; } frag_info;

in vec4 v_color;
out vec4 frag_color;

void main() {
  frag_color = v_color * frag_info.tint;
}
```

Add it to a checked-in `flutter_gpu_shaders` source manifest and compile it as a
required DataAsset from `hook/build.dart`.

```dart
import 'package:flutter_gpu_shaders/build.dart';
import 'package:hooks/hooks.dart';

void main(List<String> args) {
  build(args, (config, output) async {
    await buildShaderBundleJson(
      buildInput: config,
      buildOutput: output,
      manifestFileName: 'shaders/my_bundle.shaderbundle.json',
      assetMode: ShaderBundleAssetMode.dataAssetsRequired,
    );
  });
}
```

Load the generated DataAsset using its Flutter asset key.

```dart
final library = (await gpu.loadShaderLibraryAsync(
  'packages/my_app/flutter_gpu_shaders/shaderbundles/'
  'my_bundle.shaderbundle',
))!;
final material = ShaderMaterial(fragmentShader: library['VertexColorFragment']!);
material.setUniformBlockFromFloats('FragInfo', [1.0, 0.8, 0.4, 1.0]); // tint
node.mesh!.primitives[0].material = material;
```

A uniform block is bound by its **type** name (`FragInfo`), not its instance
name (`frag_info`). Set `ShaderMaterial.useEnvironment = true` to have the engine
bind `prefiltered_radiance` and `brdf_lut` if your shader declares them (the
diffuse-irradiance SH coefficients are not bound generically).

## std140 packing (raw `ShaderMaterial` only)

With `ShaderMaterial` you fill a single byte buffer per uniform block, and its
layout must match GLSL std140 exactly. (`.fmat` materials avoid this entirely —
the runtime packs from reflection.)

| Type | Size | Alignment | Notes |
| --- | --- | --- | --- |
| `bool` / `int` / `float` | 4 | 4 | |
| `vec2` | 8 | 8 | |
| `vec3` | 12 | **16** | pads to 16 |
| `vec4` | 16 | 16 | |
| `mat4` | 64 | 16 | four `vec4` columns |
| array element | varies | **16** | each element strides to a 16-byte boundary |

The footguns are mixing `vec3` and `float`: a `float` after a `vec3` fills the
`vec3`'s trailing pad, while a `vec3` after a `float` jumps to the next 16-byte
boundary. **When in doubt, declare blocks with `vec4`s and group trailing scalars
into `vec4`-aligned rows of four**, and the layout is unambiguous.

---

# Raw vertex shaders

A `ShaderMaterial` can supply the vertex shader as well as the fragment one.
The engine's standard vertex shader then does not run, so your shader owns
everything it used to provide.

```dart
final material = ShaderMaterial(
  vertexShader: library['RippleVertex']!,
  fragmentShader: library['RippleFragment']!,
);
material.setUniformBlockFromFloats('TintInfo', [...]);            // fragment
material.setUniformBlock('RippleInfo', bytes,
    stage: ShaderStage.vertex);                                    // vertex
```

Uniform blocks and textures are set by name on either stage, chosen with
`ShaderStage`. A block declared in both stages is two bindings, so set it twice.

## The contract

For an unskinned mesh, the engine binds these and your shader declares all of
the fixed inputs:

```glsl
uniform FrameInfo {
  mat4 camera_transform;
  vec3 camera_position;
} frame_info;

in vec3 position;
in vec3 normal;
in vec2 texture_coords;
in vec2 texture_coords_1;
in vec4 color;
in vec4 tangent;

// Instance-rate model matrix columns, bound in the slot after the vertex
// streams. A non-instanced draw gets a single-element buffer.
in vec4 model_transform_0;
in vec4 model_transform_1;
in vec4 model_transform_2;
in vec4 model_transform_3;
```

Your shader writes `gl_Position` and the standard outputs the fragment stage reads
(`v_position`, `v_normal`, `v_viewvector`, `v_texture_coords`,
`v_texture_coords_1`, `v_color`, `v_model_scale`, `v_tangent`), plus any of
your own varyings. A skinned mesh
instead takes its model transform,
`enable_skinning`, and `joint_texture_size` in `FrameInfo`, and adds the
`joints` and `weights` attributes and a `joints_texture` sampler.

## Mesh kinds and passes

A vertex shader is registered per mesh kind, so one material can customize
static meshes and leave others to the engine:

```dart
material.setVertexShader(skinnedShader, variant: MeshVariant.skinned);
material.setVertexShader(depthShader, variant: MeshVariant.depth);
```

`MeshVariant.depth` is the position-only pass that draws shadow maps and the
depth prepass. Supply it when your vertex stage moves geometry, or the shadow
keeps the undisplaced shape.

Any variant you leave unset falls back to the engine's shader for that mesh
kind, which is what makes customizing only static meshes a one-shader job. The
catch is that the engine's shader writes only the engine varyings. If your
fragment shader reads a varying your own vertex shader writes, the fallback
pairs it with a vertex shader that never writes it, and pipeline creation fails
on the backend. A debug build warns and names the missing variant when this is
about to happen. So either supply every variant your scene draws, or keep the
fragment shader to the engine varyings.

## Your own vertex layout

By default your shader is fed the engine's standard layout, which is what the
declarations above describe. To feed it something else (extra channels, a
packed format, or fewer attributes), describe the layout and bind buffers to
match:

```dart
geometry.setVertexLayout(
  VertexLayoutDescriptor(buffers: [
    VertexBufferDescriptor(
      strideInBytes: 20,
      attributes: [
        VertexAttributeDescriptor(name: 'position', format: VertexFormat.float32x3),
        VertexAttributeDescriptor(
            name: 'uv2', format: VertexFormat.float32x2, offsetInBytes: 12),
      ],
    ),
  ]),
  bindsModelTransform: false,
);
```

Attributes match the shader by name, so extra UV sets and per-vertex data are
ordinary channels rather than special cases. Pass `bindsModelTransform: false`
when your shader takes the model transform some other way than the instance-rate
slot, so the encoder leaves that slot alone.

For a single extra channel alongside the standard vertex, `setCustomAttribute`
is simpler and needs no layout (it also works on skinned meshes):

```dart
geometry.setCustomAttribute('phase', phaseValues, components: 1);
```

See `examples/flutter_app/lib/example_raw_shader.dart` with
`shaders/example_ripple.vert` and `.frag` for a worked pair.

---

# Render state

A `.fmat` material declares render state in its `material` block: `culling`
(`back` / `front` / `none`) and `blending` (`opaque` / `alpha`). A
`ShaderMaterial` exposes `cullingMode`, `windingOrder`, and `isOpaqueOverride`
constructor fields.

Today `blending` is `opaque` (depth-write on, drawn in order) or `alpha`
(depth-write off, depth-sorted, premultiplied source-over). Additive/multiply
blend modes and per-material depth state are not configurable yet; they are
encoder-controlled.

---

# Hot reload

A `.fmat` material loaded with `loadFmatMaterial` (the DataAssets workflow) hot
reloads in place. Render the scene through a `SceneView`; on hot reload it asks
the framework's hot-reload coordinator to refresh any `.fmat` whose source
changed. Every part of a `.fmat` reloads with no app-side code and no restart:

- **Render state** (`culling`, `blending`, `shading_model`) and **parameter
  defaults** — re-read from the regenerated sidecar and applied to the live
  material. A value you set at runtime (`setColor`, etc.) is preserved; an
  unset parameter takes the edited default.
- **The GLSL body** (`Surface()` and the `vertex { }` block's `Vertex()`) — the
  changed `.shaderbundle` is reloaded in place via `ShaderLibrary.reinitialize`
  and the affected render pipelines are rebuilt, so a fragment or vertex edit
  shows up live. (Changing the `varyings` / `attributes` lists changes the
  generated shaders' structure; that reloads too, but a new custom attribute
  only takes effect once the geometry supplies it via `setCustomAttribute`.)

Requirements: the DataAssets workflow (`dart run flutter_scene:init` +
`--enable-dart-data-assets`), so the build hook re-runs on a `.fmat` edit and
re-syncs the regenerated assets; and a `SceneView` (or its `reassemble` hook)
displaying the scene. A `.fmat` edit re-runs the build hook, so the reload takes
a moment while the shader recompiles. Hot reload is debug-only and tree-shaken
from release builds. (`ShaderMaterial`, the raw escape hatch below, does not
participate; it carries no sidecar.)

---

# Current state and what's next

The `.fmat` format, its preprocessor, the `buildMaterials` hook,
`PreprocessedMaterial`, the `vertex { }` stage (with custom varyings and
attributes), and hot reload are implemented. Remaining and in-flight work:

- **The `light()` hook** for a custom per-light BRDF (toon banding inside the
  engine light loop) is not implemented; use `unlit` for fully custom shading
  for now.
- **Typed codegen.** A future step will generate a typed Dart class per `.fmat`
  (compile-time-checked setters); today you use the name-based
  `MaterialParameters` API.
- **Additive/multiply blending and per-material depth state** are not yet
  configurable.
- **Per-instance custom attributes** are not exposed yet; the instance-rate slot
  carries the model transform. Per-vertex custom attributes work on both static
  and skinned meshes.
- **An inspector** that surfaces the parameter hints as UI does not exist (the
  metadata is emitted for future tooling).

---

# Troubleshooting

**`loadShaderLibraryAsync` returns null.** Confirm that the hook uses
`ShaderBundleAssetMode.dataAssetsRequired`, DataAssets are enabled, and the key
uses `packages/<app_package>/flutter_gpu_shaders/shaderbundles/<name>.shaderbundle`.
For `.fmat`, use `loadFmatMaterial` with the source `.fmat` path instead of
loading generated files directly. If a shader edit does not take effect, clear
the app's `.dart_tool` and `build` directories and rebuild.

**A `MaterialParameters` setter throws.** You used an unknown parameter name or a
type that doesn't match the declared type. The message names the parameter and
its type. Check the `.fmat` `parameters` list.

**"Failed to find uniform slot X" (raw `ShaderMaterial`).** Flutter GPU couldn't
resolve a block or sampler name. A block is bound by its type name, not its
instance name. Note that an instance name must fold (case- and
underscore-insensitively) to the block name on the GLES backend
(flutter/flutter#186394); the `.fmat` emitter handles this for you.

**Wrong colors / black geometry (raw `ShaderMaterial`).** Almost always a std140
packing mismatch; declare blocks without `vec3` members to rule it out. With a
`.fmat` material this class of bug is gone (the runtime packs from reflection).

**Black or unlit model.** For a `lit` material, confirm the scene has an
environment and/or a directional light. For raw `ShaderMaterial`, check
`useEnvironment` and that all declared samplers are bound (unbound samplers read
garbage on some backends).

---

# See also

- `examples/smoke_render/assets/custom_material.fmat` and the
  `fmat_custom_material` scene in `examples/smoke_render/lib/smoke_scenes.dart`:
  a worked `.fmat` that customizes both the vertex stage and the fragment,
  rendered through `PreprocessedMaterial`.
- `examples/flutter_app/lib/example_vertex_curve.dart` with
  `assets/vertex_ocean.fmat` and `assets/vertex_road.fmat`: the "Custom
  vertices" example (an animated curved ocean and a curved runner road),
  showing the `vertex { }` stage, custom varyings, and custom attributes.
- `examples/flutter_app/assets/toon.fmat` and `example_toon_fmat.dart`: a
  fragment-only `.fmat`; `example_toon.dart` is the raw-`ShaderMaterial` toon.
- `packages/flutter_scene/shaders/flutter_scene_standard.frag` and
  `material_lighting.glsl`: the engine's PBR shader and the lighting framework a
  `lit` material composes against.
