Blacksite/docs/editor/material-system.md
Rbanh 0798aa5d57 Build renderer and material component foundations
Add dedicated skinned rendering, pose restoration, shared Material and Material Instance slots, registry-driven components, Surface/Solari integration, transactional schema upgrades, navigation authoring, documentation, and evaluation evidence.
2026-07-12 00:24:06 -04:00

7.8 KiB

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 and ADR 0036.

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 Temporary per-renderer/per-slot runtime overrides 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.

Assign materials

  1. Select a static- or skinned-mesh actor.
  2. In Static Mesh Renderer or Skinned Mesh Renderer, find the material slot by its imported name and stable ID.
  3. Use Browse or Select to assign a Material or Material Instance. Locate reveals the current asset in the Asset Browser.
  4. Use Clear to remove only the explicit assignment. The slot returns to its read-only imported source material.

From the Asset Browser, Apply Material assigns the selected material to every material slot on each selected static or skinned renderer. Use the per-slot inspector when different draws need different assignments.

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 Asset Browser identifies the document kind, exposes its schema-driven properties and texture bindings, and uses stable subasset IDs (material:source or material:instance). 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.

Custom Surface evaluators

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

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 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 future work.

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:

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:

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.