# Materials

A **material file** is a physical material a person or an agent writes by hand: a small JSON file
whose name ends in `.material.json`. Its keys are glTF 2.0's metallic-roughness material and its
`KHR_materials_*` extensions, flattened and without the `Factor` suffix, so a glTF material maps onto
it key for key. A `preset` gives tuned starting values for common surfaces, and any key the file sets
overrides the preset.

```json
{ "preset": "brushedMetal", "roughness": 0.25 }
```

The schema is `packages/contract/src/material/` (Rust mirror: `crates/contract/src/material.rs`).
`packages/contract/material.schema.json` is the generated JSON Schema. It carries the defaults and
presets under `x-argus-material`.

## Using one

Declare the file as a scene asset of kind `material`, then name it in a `mesh` component's
`material`. The path follows the scene's asset path rule: it is relative to the scene file, with no
absolute path, drive or `..` (docs/PROJECT-MANIFEST.md, "Asset paths inside a scene").

```json
"assets": {
  "crate":  { "kind": "glb", "path": "assets/crate.glb" },
  "steel":  { "kind": "material", "path": "materials/brushed-steel.material.json" }
},
"entities": [
  { "id": "crate-1", "components": [ { "type": "mesh", "asset": "crate", "material": "steel" } ] }
]
```

A material goes on a built-in mesh the same way: `builtin:cube`, `builtin:sphere`, `builtin:capsule`,
`builtin:plane`, `builtin:cylinder` and `builtin:cone` are the six. Any other `builtin:` mesh name is refused (`asset-builtin-unknown`).

The override replaces every submesh's material, as a glTF material override always has. `mesh.color`
still multiplies into the base colour. A `material` asset that is not a `.material.json` is read as
glTF and contributes its first material, as before.

Examples are in `fixtures/materials/examples/`:

- `brushed-steel.material.json`: a preset and one override.
- `tiles/tiles.material.json`: colour, ORM and normal maps beside it, tiled 4×4.
- `neon.material.json`: an emissive, blended, double-sided sign.

## Keys

Every key is optional; `{}` is the default material. Colours are **linear** RGB(A), not sRGB and not
hex. Unknown keys are refused (see [Validation](#validation)).

| Key | glTF source | Range | Default | Notes |
| --- | --- | --- | --- | --- |
| `preset` | – | a preset name | none | See [Presets](#presets). |
| `name` | `name` | any string | none | Informational. |
| `baseColor` | `baseColorFactor` | 4 numbers, each 0..1 | `[0.8, 0.8, 0.8, 1]` | Three numbers are accepted and stored with alpha 1. |
| `baseColorTexture` | `baseColorTexture` | texture ref | none | sRGB; multiplies `baseColor`; its alpha is opacity. |
| `metallic` | `metallicFactor` | 0..1 | 0 | |
| `roughness` | `roughnessFactor` | 0..1 | 0.6 | Perceptual. |
| `metallicRoughnessTexture` | `metallicRoughnessTexture` | texture ref | none | Linear: G roughness, B metallic. |
| `ormTexture` | – | texture ref | none | Linear: R occlusion, G roughness, B metallic (an "ARM" map). Set this or `metallicRoughnessTexture`, not both. |
| `occlusionTexture` | `occlusionTexture` | texture ref | none | Linear, R. Packed into the ORM's R when the file also has an ORM or MR map. |
| `occlusionStrength` | `occlusionTexture.strength` | 0..1 | 1 | |
| `normalTexture` | `normalTexture` | texture ref | none | Linear, tangent space, +Y up (OpenGL/glTF). |
| `normalScale` | `normalTexture.scale` | any finite number | 1 | Scales X and Y. |
| `emissive` | `emissiveFactor` | 3 numbers, each 0..1 | `[0, 0, 0]` | |
| `emissiveTexture` | `emissiveTexture` | texture ref | none | sRGB; multiplies `emissive`. |
| `emissiveStrength` | `KHR_materials_emissive_strength` | 0 or more | 1 | 1 is as bright as an unlit white surface. |
| `alphaMode` | `alphaMode` | `opaque`, `mask`, `blend` | `opaque` | Lower case (glTF writes `BLEND`). |
| `alphaCutoff` | `alphaCutoff` | 0..1 | 0.5 | Only with `mask`. |
| `doubleSided` | `doubleSided` | boolean | false | |
| `clearcoat` | `KHR_materials_clearcoat` | 0..1 | 0 | |
| `clearcoatRoughness` | `KHR_materials_clearcoat` | 0..1 | 0 | |
| `clearcoatOrangePeel` | – | 0..1 | 0 | Strength of the clear coat's procedural wavy normal, the dimpled look of sprayed lacquer. Needs `clearcoat`. |
| `clearcoatOrangePeelScale` | – | millimetres, 2..30 | 8 | Wavelength of the orange peel, in object space with the instance's scale included. |
| `specular` | `KHR_materials_specular` | 0..1 | 1 | Strength of a dielectric's reflection. |
| `specularColor` | `KHR_materials_specular` | 3 numbers, each 0 or more | `[1, 1, 1]` | |
| `ior` | `KHR_materials_ior` | 1 or more | 1.5 | |
| `transmission` | `KHR_materials_transmission` | 0..1 | 0 | |
| `sheenColor` | `KHR_materials_sheen` | 3 numbers, each 0..1 | `[0, 0, 0]` | HD only; see [What each renderer draws](#what-each-renderer-draws). |
| `sheenRoughness` | `KHR_materials_sheen` | 0..1 | 0 | HD only. |
| `subsurface` | – | 0..1 | 0 | Share of the diffuse light that scatters under the surface, as in skin. HD blurs it in screen space; see [Skin and hair](#skin-and-hair). More than 0 turns the clear coat off. |
| `subsurfaceRadius` | – | 3 numbers in metres, each 0 or more | `[0.0022, 0.00072, 0.00034]` | How far light scatters per channel (Burley's diffusion distance `d`). The default is skin's: red travels furthest. |
| `anisotropyStrength` | `KHR_materials_anisotropy` | 0..1 | 0 | HD only. The highlight stretches along the direction, as on brushed metal. |
| `anisotropyRotation` | `KHR_materials_anisotropy` | radians | 0 | Turns the direction from the mesh tangent toward the bitangent. |
| `anisotropyTexture` | `KHR_materials_anisotropy` | texture ref | none | Linear: RG a tangent-space direction, B scales `anisotropyStrength`. |
| `anisotropyPattern` | – | `uv`, `radial`, `circular` | `uv` | Where the brushed direction points: along the mesh tangent (`uv`, as in glTF), outward from the part's centre (`radial`), or around its axis (`circular`, machined wheel faces). The centre is the mesh's local origin unless `anisotropyTexture` is given. |
| `anisotropyAxis` | – | `x`, `y`, `z` | `y` | The mesh-local axis `radial` and `circular` turn about. Wheels from DCC tools often spin about local X, so a scene sets this; `wheelAluminium` leaves it unset. |
| `iridescenceFactor` | `KHR_materials_iridescence` | 0..1 | 0 | HD only. A thin film's interference colours, as on soap bubbles or oil. |
| `iridescenceIor` | `KHR_materials_iridescence` | 1 or more | 1.3 | The film's index of refraction. |
| `iridescenceThicknessMaximum` | `KHR_materials_iridescence` | nanometres, 0 or more | 400 | glTF's thickness runs from a minimum to this maximum through a thickness map. HD reads no map yet and uses this as the film's thickness. |
| `diffuseTransmissionFactor` | `KHR_materials_diffuse_transmission` | 0..1 | 0 | HD only. Share of the diffuse light that passes through a thin surface lit from behind: leaves, paper, lampshades. |
| `diffuseTransmissionColorFactor` | `KHR_materials_diffuse_transmission` | 3 numbers, each 0..1 | `[1, 1, 1]` | Tint of the transmitted light. |
| `flakeColor` | – | 3 numbers, each 0..1 | none (off) | Colour of the metallic flakes under the clear coat. The `carPaint` preset sets it to the resolved `baseColor` × 1.2. |
| `flakeDensity` | – | 0..1 | 0.5 | Share of the surface the flakes cover. Needs `flakeColor`. |
| `flakeSize` | – | millimetres, 0.05..2 | 0.3 | Flake cell size in object space, instance scale included, so it does not swim with the UV scale. Below about one pixel the flakes fade to their averaged tint. Needs `flakeColor`. |
| `flakeSparkle` | – | 0..1 | 0.5 | How strongly each flake's random orientation makes it glint (the spread of the flake normals). Needs `flakeColor`. |
| `flipColor` | – | 3 numbers, each 0..1 | none (off) | Colour seen at grazing angles: a two-tone or colour-flip paint, blended by view angle. |
| `flipStrength` | – | 0..1 | 1 | How far `flipColor` replaces the base colour at grazing angles. Needs `flipColor`. |
| `layers` | – | up to 3 of `{ material, mask, contrast }` | none | HD only. Materials blended over this one before lighting, in order; see [Layers](#layers). |
| `layerMask` | – | texture ref | none | Linear. The layers' mask, read with `uvTransform`. Absent: the vertex colour masks the layers. |
| `uvTransform` | `KHR_texture_transform` | `{ offset, scale, rotation }` | identity | Applies to every texture. `scale` is tiling and is non-zero; `rotation` is radians. |
| `shading` | `KHR_materials_unlit` for `unlit` | `pbr`, `unlit`, `vertex`, `toon`, `hair` | the preset's, else the scene's | How the surface is lit; see [Shading and filtering](#shading-and-filtering). |
| `toonSteps` | – | whole number 1..16 | 3 | Light bands of `toon` shading. |
| `filter` | – | `linear`, `nearest`, `three-point` | the scene's | How the textures are filtered; see [Shading and filtering](#shading-and-filtering). |
| `shader` | – | asset id or path | none | A `*.shader.wgsl` whose `surface()` draws the material; see [Shader files](#shader-files). |
| `params` | – | object | none | Values for the shader's `param` and `texture` names; needs `shader`. |
| `castShadows` | – | `true`, `false` | `true` | Whether the surface casts shadows. |

The defaults are glTF's, with three exceptions: `baseColor`, `metallic` and `roughness` are the
engine's builtin grey dielectric. glTF's own defaults describe a white rough metal. A hand-written
file that sets only `baseColor` means paint, not metal. A glTF importer writes every key it has, so
the difference never reaches an import.

`uvTransform` follows KHR_texture_transform: `uv' = offset + R(rotation) × (scale × uv)`. A glTF
import has already baked its transform into the model's UVs, so an imported material never carries
one.

### Shading and filtering

`shading` and `filter` say how the surface is drawn rather than what it is made of. Absent, a
material takes its preset's shading (only `hair` sets one), else the scene's `meta.render.shading` and `meta.render.filter` (docs/RENDERING.md,
"Stylised looks"), which default to `pbr` and `linear`. So one scene-level word restyles every
material, and a material that sets its own keeps it.

| `shading` | What it draws |
| --- | --- |
| `pbr` | Lit per pixel: GGX specular, Lambert diffuse, the sky's image-based light. |
| `unlit` | `baseColor` × colour map × vertex colour, exactly: no light, no tonemapping, no exposure. |
| `vertex` | Gouraud: diffuse light and the sky's ambient worked out at each vertex and interpolated across the triangle, no specular. The sun's shadow still falls per pixel. The late-1990s look. |
| `toon` | Diffuse light in `toonSteps` bands plus a rim. |
| `hair` | Hair cards: fibre highlights running across the strands, which follow the mesh tangent (HD); see [Skin and hair](#skin-and-hair). The standard renderer draws it as `pbr`. |

Every mode multiplies in the mesh's vertex colours (glTF `COLOR_0`).

| `filter` | What it draws |
| --- | --- |
| `linear` | Bilinear with mipmaps, or whatever a glTF model's sampler says. |
| `nearest` | The nearest texel: hard texel edges when a texture is magnified. |
| `three-point` | Each pixel blends the three nearest texels (a triangle, not a square), the filter some late-1990s console hardware used. Four texture reads instead of one. |

A glTF model's texture sampler (its filters and its wrap modes: repeat, clamp, mirrored repeat) is
honoured; a material file's textures repeat and filter linearly unless `filter` says otherwise.

```json
{ "baseColorTexture": "bricks.png", "shading": "vertex", "filter": "nearest" }
```

This is not a fourth material. `unlit` and `toon` are the engine's unlit and toon materials, so HD
draws them too. `vertex`, `filter` and the scene defaults are drawing styles of the standard
renderer's one surface pipeline; HD lights a `vertex` material per pixel and filters linearly.

To see a material before placing it, capture it on a preview shape: `POST /capture` with
`{"kind": "material", "material": "<asset id or the file's JSON>", "shape": "sphere"}`
(`play.capture` takes the same), drawn alone under the scene's sky, sun and look.

### Texture references

A texture reference is either of these:

- **A scene asset id** of kind `texture`, e.g. `"brick-albedo"`. The file is that asset's `path`.
  Use this for textures several materials share.
- **A path relative to the material file's folder**, e.g. `"tiles-albedo.png"` for a file beside it.
  As with scene asset paths, an absolute path, a drive or a `..` segment is refused
  (`material-texture-invalid`), so the material moves with its folder. Keep a material and its maps
  in one folder: `materials/tiles/tiles.material.json` beside `materials/tiles/tiles-*.png`.

The id is tried first. PNG, JPEG and KTX2 load through the same loader as glTF textures, and read
from the exported payload in a shipped game. Colour and emission maps are read as sRGB and every
other map as linear data, whatever the file says.

A texture that does not load leaves its slot empty and prints one warning. The material still
loads, even in strict mode. An editor build (Play, test workers, export) counts every texture a
material file names by path as one of the scene's files. The build packs each one, refuses to run
when one is missing, and marks the artifact stale when one changes.

## Shader files

A material with a `shader` is drawn by that shader instead of by glTF's factors and maps. The shader is
a `*.shader.wgsl` file (asset kind `shader`): a short header of `//!` lines, then WGSL with a
`surface()` function. `docs/SHADERS.md` has the format; this section is what the material file adds.

```json
{ "shader": "shaders/dissolve.shader.wgsl",
  "params": { "edgeColor": [0.2, 0.6, 1.0], "albedo": "crate.png" },
  "castShadows": true }
```

- `shader` is a scene asset id of kind `shader`, or a path relative to the material file, with the
  texture path rule (no leading slash, drive or `..`). A file that is not there is
  `material-shader-missing`; a fault in its header is reported under the header's own code
  (`shader-header-invalid`, `shader-param-invalid`, `shader-texture-invalid`, `shader-model-mismatch`)
  with the shader's `file:line:col`.
- Each `params` key must be a `param` or `texture` the shader declares, else
  `material-shader-param-unknown` (it suggests the nearest name; an `instance` value is set per entity
  by a behaviour with `ctx.render.setShaderParam`, not here). A value of the wrong type is `shader-param-invalid`; outside the declared
  `[min, max]` it is `material-shader-param-range`.
- A colour is 3 linear numbers or `#RRGGBB`. A texture is `white`, `black`, `flat`, `builtin:noise` or a
  path relative to the material file.
- `baseColor`, every texture map, `shading` and `layers` do nothing on a material with a shader
  (`material-field-ignored`, a warning). `doubleSided`, `alphaCutoff`, `castShadows` and the extension-lobe
  keys still apply. `params` without a `shader` also warns.

## Presets

A preset sets only the keys it lists; the rest are the defaults. Colours are linear. `carPaint` also derives its `flakeColor` at expansion: the resolved `baseColor` RGB × 1.2, so a file's own `baseColor` paints the flakes too; a `flakeColor` in the file wins.

| Preset | What it is |
| --- | --- |
| `plastic` | Smooth-ish white dielectric, a moulded plastic housing; set baseColor for the colour. |
| `paintedMetal` | Satin paint over metal: a dielectric (the paint is what you see), a little rougher than plastic. |
| `brushedMetal` | Brushed aluminium: full metal, medium roughness (the brushing direction is not modelled). |
| `polishedMetal` | Polished steel or chrome: full metal, near-mirror. |
| `rubber` | Black rubber: dark, rough, a weak reflection. |
| `glass` | Clear glass: smooth, fully transmissive, ior 1.5 (HD draws it as thin glass). |
| `carPaint` | Metallic car paint: a coloured base with flakes (flakeColor = baseColor * 1.2, whatever baseColor the file sets) under a full clear coat with a light orange peel. |
| `wheelAluminium` | Machined aluminium wheel face: full metal, brushed in circles about the wheel's axis. The axis is unset (y): wheels from DCC tools often spin about local X, so a scene sets anisotropyAxis. |
| `wheelGlossBlack` | Gloss black wheel: near-black dielectric under a smooth clear coat. |
| `tyreRubber` | Tyre rubber: dark, rough, with a dusty grey sheen. Lettering comes from the metallicRoughnessTexture or ormTexture roughness channel (smoother and darker where the letters are). |
| `fabric` | Cloth: fully rough with a soft sheen and a weak reflection (HD draws the sheen). |
| `wood` | Unfinished wood: a warm brown dielectric, fairly rough; pair it with a grain texture. |
| `concrete` | Cast concrete: light grey, matte. |
| `emissiveLight` | A lit panel or bulb: warm white emission at 5x unlit white, so it blooms. |
| `skin` | Human skin: light scatters under the surface (HD), and the reflection is weak (F0 0.028); set baseColor for the tone. |
| `hair` | Hair cards: fibre highlights along the mesh tangent (HD), alpha-masked strands, both sides; set baseColor for the hair colour. |

The numbers are the `MATERIAL_PRESETS` table in `packages/contract/src/material/presets.ts`. It is
emitted into `crates/contract/src/gen.rs` and `material.schema.json`, so both languages resolve a
preset from one table. Resolution happens in three layers, and each later layer wins: the defaults,
then the preset, then the file's keys.

## Validation

Both languages report the same violations, with the same codes, pointers and messages
(`fixtures/materials/manifest.json` is the shared list; `argus-validate --material <file>` prints
the Rust verdict and the resolved material).

| Code | Severity | When |
| --- | --- | --- |
| `schema-shape` | error | Not JSON, not an object, a wrong type, an **unknown key**, or a `preset` / `alphaMode` / `shading` / `filter` that is not a member. |
| `material-value-range` | error | A number outside its range, as in the table above; more than 3 `layers`; a layer's `contrast` below 1. |
| `material-texture-invalid` | error | A texture reference, or a layer's `material` reference, is empty, absolute, names a drive or has a `..` segment; or `ormTexture` and `metallicRoughnessTexture` are both set. |
| `material-field-ignored` | warning | A key that has no effect: `alphaCutoff` without `mask`, `normalScale` without a normal map, `occlusionStrength` without an occlusion or ORM map, an `emissiveTexture` or `emissiveStrength` on a black `emissive`, `clearcoatRoughness` with no clear coat, `sheenRoughness` with no sheen, `clearcoat` on a material with `subsurface` above 0, `subsurfaceRadius` while `subsurface` is 0, `subsurface` with `hair` shading, `anisotropyRotation`, `anisotropyTexture`, `anisotropyPattern` or `anisotropyAxis` while `anisotropyStrength` is 0 (the axis also with `uv` brushing), `clearcoatOrangePeel` with no clear coat, `clearcoatOrangePeelScale` with no orange peel or clear coat, `flakeDensity`, `flakeSize` or `flakeSparkle` without `flakeColor`, `flipStrength` without `flipColor`, `iridescenceIor` or `iridescenceThicknessMaximum` while `iridescenceFactor` is 0, `diffuseTransmissionColorFactor` while `diffuseTransmissionFactor` is 0, `layerMask` without `layers`, `sheenColor`, `anisotropyStrength`, `iridescenceFactor` or `diffuseTransmissionFactor` on a skin (`subsurface` above 0) or `hair` material, `toonSteps` without `toon` shading, a `filter` with no texture. |

Unknown keys and names come with the key they were probably meant as:

```
unknown key "baseColorFactor"; did you mean "baseColor"?
unknown key "normal_texture"; did you mean "normalTexture"?
uvTransform: unknown key "tiling"; did you mean "scale"?
preset "car-paint" is not one of plastic, paintedMetal, …; did you mean "carPaint"?
alphaMode "BLEND" is not one of opaque, mask, blend; did you mean "blend"?
shading "gouraud" is not one of pbr, unlit, vertex, toon; did you mean "vertex"?
```

The suggestion is chosen in this order. First comes glTF's own spelling of the key, plus a few
common guesses (`color`, `albedo`, `metalness`, `tiling`). Then the camelCase of a snake_case or
kebab-case spelling, then a case-insensitive match, then the closest key within two edits.

glTF's trap is worth knowing. `emissiveFactor` defaults to black, and the emission map multiplies
it, so an `emissiveTexture` on its own glows nowhere. `material-field-ignored` says so. Set
`emissive` to `[1, 1, 1]` to use the map as it is.

At load, a material file that does not validate fails a strict load (`LoadOpts::placeholder_assets`
off) with its violations, as a model that does not load does. Otherwise (the default) the builtin
material stands in, and one warning names the file and the first violation. Warnings print once
per key.

## Skin and hair

Characters use two presets, and each keeps working when its keys are overridden.

```json
{ "preset": "skin", "baseColor": [0.62, 0.42, 0.33] }
{ "preset": "hair", "baseColorTexture": "strands.png", "baseColor": [0.2, 0.12, 0.06] }
```

- **Skin** (`subsurface` above 0).
  - HD takes that share of the pixel's diffuse light and spreads it over the surface around it, by
    `subsurfaceRadius` per channel. The kernel is the screen-space separable one from Jimenez et
    al. 2015, fitted to Burley's diffusion profile.
  - The scattering moves light and keeps its total: soft terminators, red bleeding into shadow
    edges, softened pores.
  - The colour map is applied half before and half after the scattering, so texture detail
    stays sharp.
  - A scattering material has no clear coat.
  - Cost on an RTX 5080 at 1440p: about 0.06 ms when skin fills half the screen, and nothing
    when no skin is in view. Quality knob `materials.sss` (off on Low).
- **Hair cards** (`shading: "hair"`).
  - Each card's strands run along its mesh tangent (the direction of increasing U in a glTF
    model's tangents), and the highlights run across them.
  - The lobes follow Karis 2016: a white highlight toward the root, a tinted one toward the tip,
    and light through the fibres from behind.
  - Use `alphaMode: "mask"`. Texels at or above `alphaCutoff` are opaque. The thinner fringe
    below it is dithered, and the temporal upscaler turns the dither into soft coverage.
  - The quality knob `hair.blendedFringe` (Ultra) blends the fringe in the order-independent
    transparency pass instead. It looks softer at silhouettes, and costs about three times as
    much.
  - Cost on an RTX 5080: about 0.09 ms for a close-up head of 200 cards.
- **Non-character uses.** Both are general. `subsurface` suits wax, marble, jade or leaves;
  `hair` shading suits fur, grass cards and any strand-like card.

## Layers

`layers` blends up to three other materials over this one, in order, before the surface is lit.
This is how dirt over paint, wear over metal or a wet patch over asphalt is drawn. Each entry is:

| Key | Values | Default | Notes |
| --- | --- | --- | --- |
| `material` | a scene asset id of kind `material`, or a `.material.json` path relative to this file | required | The layer's own layers and HD-only effects are not read, so layers cannot cycle. |
| `mask` | `r`, `g`, `b`, `a` | `r` | The mask channel. |
| `contrast` | 1 or more | 1 | Sharpens the mask around 0.5; 1 keeps it as is. |

The mask is `layerMask`, read with this material's `uvTransform`. Without it, the mesh's vertex
colour is the mask, and it no longer tints the surface. Each layer reads its own textures with its
own `uvTransform`, so a dirt layer can tile on its own. Base colour, normal, roughness, metallic,
occlusion, emission, clear coat and specular blend.

A layer that does not load is left out, with one warning. A layer file, or a texture it names, that the project does not hold is also said before the run: `build.material-layer-missing` on the scene write and in the build. The standard renderer draws the base
material only.

```json
{ "preset": "paintedMetal", "layers": [{ "material": "rust.material.json", "contrast": 3 }], "layerMask": "wear.png" }
```

## Terrain layers

A terrain blends up to eight layers, each an existing material file
named in the terrain's `layers` (`material`, a scene asset id or a project-relative path). What a
layer takes from its file: `baseColor` times `baseColorTexture` (the colour), `normalTexture`, `roughness`, and
`occlusionTexture` as its height map (without one, the colour's luminance). Everything else in the
file is ignored for a layer. The layer's own `uvScaleM` is the metres one tile covers, and
`triplanar: "auto"` (the default) lets it map from two world directions on faces steeper than 40
degrees; `"off"` keeps it top-down.

A pixel of terrain shades two layers, however many the terrain has: the two heaviest among the four
samples around it, blended by height (the higher layer's texture fills between the lower one's),
whether the standard renderer or HD draws it. The standard renderer swaps to a baked far colour map
(one texel a metre) beyond 64 m; HD shades the layers directly at every distance.

## What each renderer draws

| | Standard renderer | HD (`render.profile: "hd"`) |
| --- | --- | --- |
| Base colour, colour map, metallic, roughness, normal map | yes | yes |
| ORM / metallic-roughness / occlusion maps, occlusion strength | no | yes |
| Emission colour and strength | yes: added to the lit colour before exposure and the tonemap; `unlit` surfaces ignore it | yes |
| Emission map | no | yes |
| Alpha mask and blend, double-sided | yes (blend sorted back to front per object) | yes |
| `shading` | yes | `unlit` and `toon`; `pbr` and `vertex` are lit per pixel |
| `filter`, a glTF sampler | yes | linear |
| Clear coat, specular, specular colour, ior | no (a second lobe only for the car keys below) | yes (ior folds into the specular tint) |
| Transmission | no | as thin glass: blended by what it does not transmit, no refraction |
| `uvTransform` | no (the mesh's own UVs) | yes, in every pass: raster, shadow, virtual geometry and ray tracing |
| Sheen | no | yes (Charlie sheen over the base, `KHR_materials_sheen`) |
| Anisotropy | only with the car keys below: a stock shader stretches the highlight along `anisotropyPattern`/`anisotropyAxis` | yes (not on skin or hair materials) |
| Iridescence, diffuse transmission | no | yes (not on skin or hair materials) |
| Flakes (`flakeColor` and tuning), orange peel, flip colour, clear coat on the new car keys | yes: a stock surface/light module the engine ships, drawn through the shader path (cell-noise flake normals in object-space millimetres, faded to the averaged tint below about one pixel; clear coat as a second specular lobe) | yes: HD's car-paint layers |
| `layers` | no: the base material | yes, blended before lighting |
| `subsurface`, `subsurfaceRadius` | no | yes: screen-space subsurface scattering (quality knob `materials.sss`) |
| `hair` shading | as `pbr` | yes: fibre lobes along the tangent; the fringe under `alphaCutoff` dithered, or blended with `hair.blendedFringe` |

**Car paint, wheels and tyres in the standard renderer.** A material file that uses a car preset
(`carPaint`, `wheelAluminium`, `wheelGlossBlack`, `tyreRubber`) or any car key (`flakeColor`,
`flipColor`, `clearcoatOrangePeel`, a non-`uv` `anisotropyPattern`), on an opaque pbr material without
a `shader`, is drawn with a stock surface module the engine ships, through the same path as a user
shader. It is built for the keys the material uses, so a plain clear coat does not pay for flakes.
Flakes are a cell noise of random normals sized in object-space millimetres (the instance scale is
included) with a glint under each light, and fade to their averaged tint below about a pixel, so they
do not shimmer at distance. Orange peel is a low-amplitude wavy normal on the clear coat, faded the
same way. The colour flip blends by view angle. The clear coat is a second specular lobe; anisotropy
bends the normal along the pattern (the highlight stretches along the pattern's direction, as in glTF;
`radial` and `circular` turn about `anisotropyAxis` of the mesh's local origin). Sheen is a dusty rim.
The clear coat reflects the scene's sky or environment, and flakes glint under the sun and every local light. The stock modules draw no normal map. A web, Android or iOS build, whose shader cost cap the full module exceeds, gets a lighter set (no orange peel, colour flip or anisotropy; flakes as a static speckle with an averaged glint; one lobe whose roughness follows the clear coat). The module ships in an exported game's shader pack and pre-warm list, so a shipped build draws it too (an HD export draws these keys natively and needs none). Materials without these keys draw
exactly as before.

The standard renderer draws a material file exactly as it draws a glTF material. It uses the
PBR-lite of the colour, roughness, metallic, colour map and normal map (`PbrMaterial::lite`), with
the material's shading, filter, alpha and sidedness (a `MaterialStyle` registered beside it). Its
frames of existing opaque scenes are unchanged. When a material file sets something only HD draws,
the standard renderer prints one line naming those keys. The maps only HD reads (ORM, occlusion,
emission) are loaded only under HD. Emission colour times strength is the exception: the standard
renderer adds it to a lit surface's colour (pbr, toon and vertex), so a glowing material needs no
`unlit` shading. A material with no emission renders byte for byte as before.

Alpha in the standard renderer: `mask` cuts texels below `alphaCutoff` out of the colour, depth and
id buffers; `blend` draws after every opaque surface and the sky, each blended object on its own,
farthest first by its centre, without writing depth. Two limits: shadows are cast by the whole
surface either way, and a blended surface keeps the tonemap state of what is behind it, so under
`aces` or `neutral` an *unlit* blended surface over a lit one is tonemapped with it (with
`tonemap: "none"` there is no difference).

## Reloading

The running world caches a material file per asset id. It re-reads the file when the scene's asset
entry changes, which a hot reload and a scene switch both detect, and on a lost device. Editing the
`.material.json` in place, or one of its textures, is not seen by a running `argus run`: its watcher
watches the scene file, as it does for models. The exception is a material with a `shader`: under
`--watch` the watcher also covers its shader file (reloaded in the background, the old version drawing
until the new one is built) and the material file's `params`, which are re-read and written to the
shader's buffer (docs/SHADERS.md, "Editing live"); a changed `shader` key or texture still needs a scene reload. In the editor, the material file and every texture
it names are build inputs, so an edit makes the current artifact stale and the next Play loads it.

