# Material System

Blacksite uses shared project materials and renderer-owned material slots. Static and skinned mesh
renderers expose the same assignment workflow, but remain separate components with separate mesh
and hydration ownership. The permanent contracts are [ADR 0035](../adr/0035-shared-material-assets-and-renderer-slots.md)
and [ADR 0036](../adr/0036-surface-abi-and-solari-parity.md).

## Mental model

| Concept | Owns | Saved? |
|---------|------|--------|
| Material asset | Reusable shader choice, render state, parameters, and textures | Yes, under `assets/materials/` |
| Material instance | Overrides over one direct base material | Yes, under `assets/materials/` |
| Renderer material slot | A stable reference on one static or skinned renderer | Yes, in scenes/prefabs |
| Imported source material | Read-only default supplied by model import | Yes, as the slot fallback |
| Material property block | Reserved runtime-only per-renderer/per-slot override schema; application/promotion tracked by Gitea #53 | No |

A renderer slot stores a `MaterialRef`, not a copy of the material. The registry UUID and subasset
ID identify the target; the cached path is only a loading hint. A material instance may reference a
material, but may not reference another instance.

`MaterialPropertyBlocks` are excluded from authored persistence, but they are not yet consumed by
renderer binding. Runtime application and the transactional **Promote to Material Instance**
workflow are tracked by Gitea #53. Use an ordinary direct-base Material Instance for reusable
variations until that issue closes.

## Assign materials

Open **Window > Material Library** for the cross-folder Material/Material Instance catalog. It
supports search, Material/Instance/scene-usage filters, grid/list thumbnails, dependency health,
scene-reference counts, creation, drag sources, and the guarded shared-asset editor. It lives in the
existing bottom dock and does not add permanent viewport chrome. Scene-usage rows select the exact
actor, focus the viewport, and identify its renderer slot, primitive surface, or brush face.

Assign through either exact workflow:

1. Drag a Material or Material Instance from the Material Library or Asset Browser onto the desired
   viewport surface. A hydrated static/skinned draw targets its exact stable renderer slot, a
   primitive targets its actor material, and a brush targets the exact face under the pointer.
2. Check the pointer card and target visual. Green names a valid actor/slot/face; red gives the
   blocking reason. Release commits one undoable assignment. Move away, leave the viewport, press
   Escape, or right-click to restore the pre-drag state without history or dirtying the scene.
3. Alternatively, select a static- or skinned-mesh actor and use **Browse** or **Select** on the
   named slot in its renderer inspector. **Locate** reveals the current asset in the Asset Browser;
   **Clear** removes only the explicit assignment and restores the read-only imported source.

From the Asset Browser, **Apply Material** assigns the selected material to every material slot on
each selected static or skinned renderer. This is the explicit bulk path. A viewport hit never
silently broadens to all slots.

Texture drops target a primitive's base-color texture or one brush face. Direct Texture drops on a
renderer slot are rejected because a renderer slot stores a Material reference; create or edit a
Material Instance and assign the texture there.

See the [Material Library and targeted-drop evaluation](evaluations/material-library-targeted-drop/)
for the live preview, commit/undo, cancel, and source-verification record.

Static draw slots and material slots are separate. Removing a static draw retains its explicit
material assignment as an orphan rather than guessing a replacement. Reimport also reconciles
slots by stable ID, never by display name. Resolve an orphan explicitly in the renderer inspector.

## Author shared assets

Material files and material-instance files are RON documents under `assets/materials/`. The
Material Library and Asset Browser identify the document kind, expose the same guarded
schema-driven editor and texture bindings, and use stable subasset IDs (`material:source` or
`material:instance`). Material Instance thumbnails resolve the direct base plus sparse overrides.
Built-in shader schemas live under `assets/shaders/`.

Select a Material and use **Create Instance** to create a direct-base variant beside it. The
instance inspector exposes its base Material plus sparse property and texture override checkboxes;
unchecked values continue to inherit. Material instances cannot inherit from other instances.
Material render state exposes Opaque/Cutout, alpha cutoff, and Double Sided in the base Material
inspector.

Every standard and shader-schema texture slot uses the project texture picker. **Browse** lists
texture assets across the project, including imported model texture subassets, and a texture can be
dragged from the Asset Browser directly onto a slot. Non-texture drops are ignored. **Clear** removes
the base Material value; on a Material Instance it removes the sparse override so the slot inherits
from its base again. Dropping or browsing a texture onto an unchecked instance slot creates the
override automatically.

The supported render states are:

- **Opaque** — no alpha candidate test.
- **Cutout** — alpha is compared with the material's cutoff in raster and, for eligible geometry,
  at Solari ray candidates.
- **Double sided** — stored on the shared material render state.

Editing a shared asset never writes copies into renderer components. The runtime watches the
Material, direct base, shader schema, and evaluator dependencies and updates the shared hydrated
handle in place. Standard and custom Surface changes therefore propagate to every assigned static
or skinned renderer slot without reloading geometry, joints, or the current animation pose. Invalid
Surface edits retain the last-good evaluator generation and emit diagnostics.

Renderer and terrain material bindings target generated draw entities. Scene switches may remove
those entities between binding discovery and deferred command application, so binding replacement
uses fallible entity commands and treats that stale-target race as normal lifecycle cleanup rather
than an editor-fatal error.

## Custom Surface evaluators

A custom shader schema may reference evaluator WGSL. Evaluators return material properties through
one constrained entry point:

```wgsl
fn evaluate(
    input: SurfaceInput,
    params: SurfaceParams,
    samples: SurfaceSamples,
) -> Surface {
    var surface = surface_default();
    surface.base_color = samples.values[0];
    surface.perceptual_roughness = params.lanes[0].x;
    return surface;
}
```

Surface ABI v1 supplies UV0, world position, world normal, 16 parameter lanes, and eight sampled 2D
textures. The evaluator returns base color/alpha, tangent-space normal, emissive, metallic,
roughness, reflectance, occlusion, and lit/unlit model selection. Mesh tangents are required for a
custom tangent-space normal to affect raster output and for Solari mesh compatibility.

Do not declare bindings or `@vertex`, `@fragment`, or `@compute` entry points. Storage resources,
ray queries, derivatives, discard, barriers/subgroups, texture stores, and atomics are rejected.
The engine owns the render stages and composes the same validated evaluator into raster and Solari.

## Solari scope

For eligible non-deformed triangle geometry, Solari uses the same Surface evaluator, packed
parameters, textures, normal, emissive/unlit choice, and cutout alpha decision as raster rendering.
The standard material path remains unchanged for materials without a custom evaluator. See the
[rendering guide](rendering.md) for GI selection and fallback diagnostics.

Skinned meshes and morph-deformed meshes are currently excluded from Solari. This prevents rays
from tracing their undeformed source/bind pose while raster shows the animated pose. They remain
visible in raster, and Rendering diagnostics report the deformed exclusion. Dynamic deformed
vertex/BLAS updates are tracked independently by Gitea #54 with animation, ray-hit, fallback,
memory, and update-cost acceptance.

Surface ABI v1 does not support blended transparency, transmission/refraction, custom vertex
displacement, or arbitrary pipeline stages.

`assets/materials/surface_tint.ron` and `assets/shaders/surface_tint.*` are the committed minimal
custom-Surface example used for editor and renderer verification.

## Upgrade and validate a project

Normal project loading does not rewrite material or scene files. Preview the explicit transactional
upgrade first:

```bash
cargo upgrade-project --project .
cargo upgrade-project --project . --apply
```

Apply mode stages the complete rewrite, creates a timestamped backup under
`.blacksite/backups/material-component-v4-*`, and rolls back on failure. Commit or otherwise back up
the project before applying, inspect the diff afterward, then run:

```bash
cargo --locked validate-levels
```

Validation reports unresolved material/schema/texture references, nested or incompatible instance
bases, and unsupported project content through the same editor/headless finding model.

## Troubleshooting

| Symptom | Check |
|---------|-------|
| Clearing a slot appears to do nothing | Clear removes the explicit override; the imported source material is still effective. |
| Assignment disappeared after reimport | Look for an orphaned assignment. Slots are intentionally not matched by name. |
| Skinned material edit resets the pose | This is a regression: material-only changes must patch the existing hierarchy rather than reload it. |
| Custom evaluator is rejected | Check the required `evaluate` signature, ABI lane/texture limits, and forbidden constructs. |
| Custom normal has no visible effect | Verify the mesh has tangents. |
| Animated actor is missing from Solari GI | Expected for current skinned/morph geometry; inspect the deformed-exclusion diagnostic. |
| Material reference is unresolved | Run project validation and repair/reselect the stable asset reference; do not hand-edit a display-name match. |
| Texture drop is red on an imported renderer | Renderer slots accept Materials, not loose textures. Create/edit a Material Instance, set its texture, then assign it. |
| Drag preview remains after leaving a target | This is a regression: target changes, viewport exit, Escape, right-click, and invalid release must restore the exact snapshot. |
