Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR 0007: Props, cut-out materials and impostors

Status: accepted, 2026-09-26.

Context

ashlar bakes buildings: recipes of parts meshed by a kernel into levels of detail, dressed in opaque tiling materials. A game also draws things that are not buildings and are not made from a recipe of solids: trees, bushes, rocks, cliffs, debris. A game generates those with its own code, and what it needs from ashlar is the rest of the pipeline it already has for buildings: a baked file with levels and colliders, materials baked from graphs, a loader, a way to draw thousands of them, and something cheap to draw far away.

Four things are missing:

  • Materials are opaque. A leaf card needs its alpha cut out, light through it from behind, and both sides drawn. PbrOutput has no opacity, MaterialDefinition no alpha mode, and ADR 0003 put transparency out of scope.
  • Meshes carry no data of their own. TriangleMesh has positions, normals and metre UVs. A tree's wind needs a pivot per vertex and a stylised shader wants ambient occlusion and a height gradient; there is nowhere to put either.
  • The only baked asset is a building. A tree dressed as a building would need a recipe of solids it does not have, and would carry rooms and portals it never uses.
  • Nothing is cheap far away. A building's far level is still its geometry. A forest's far level has to be a picture.

Decision

Cut-out, two-sided and translucent materials

MaterialDefinition gains three fields, all defaulting to today's behaviour and skipped when default so every library written before them reads and writes byte for byte as it did:

  • alpha: Alpha, Opaque or Mask { cutoff }. Blending is out of scope: sorted transparency is a renderer problem, and every foliage path worth shipping cuts out.
  • double_sided: bool, which also turns off back-face culling.
  • diffuse_transmission: f32, 0..=1, the share of diffuse light that passes through, which is how a leaf lit from behind glows.

PbrOutput gains an optional opacity input, baked into the base colour map's alpha, with a coverage-preserving mip chain (each level's alpha is rescaled so the fraction of texels over the cutoff matches level zero), so a card does not thin away with distance. create_material maps all three to StandardMaterial.

Meshes carry named vertex data

TriangleMesh gains colors: Option<Vec<[f32; 4]>> and attributes: BTreeMap<String, VertexData>, where VertexData is one of F32, F32x2, F32x3, F32x4 or U32, one value per position. Both default empty, and a building never sets them. ashlar-bevy uploads colours as Mesh::ATTRIBUTE_COLOR and each named attribute as a MeshVertexAttribute whose id is a stable hash of its name, exposed as ashlar_bevy::attribute(name, format), so a game's shader specialisation names the same attribute without either side agreeing on a number.

A prop is a baked asset beside a building

ashlar::prop holds a Prop: an id, levels of detail, colliders and a bounding box, and no recipe. A PropLevel has an until distance, whether it casts shadows, and its PropPieces: a TriangleMesh and a material key. Its last level may instead be an impostor (below): an ImpostorRef, the keys of the two atlas files and the ImpostorLayout they were baked with. A PropCollider is a capsule, a cylinder or a convex hull, in the prop's own frame. The file is the building file's container (identifier, version, a RON manifest, a binary mesh table) with its own identifier, written by ashlar-content's Content::write_prop and read by plain ashlar, so a server reads a prop's colliders with no Bevy and no kernel.

ashlar-bevy loads .ashprop files into a PropAsset whose meshes are labelled sub-assets, created once however many copies are drawn, and spawns one with an AshlarProp { prop, materials } component the way it spawns a building: one child per piece per level, each with its VisibilityRange. A game drawing tens of thousands, such as a forest, takes the asset's mesh and material handles and manages its own entities; the component is for the handful of props a scene places by hand.

Impostors are baked pictures of a prop

ashlar-impostor bakes a prop's nearest level into a hemi-octahedral impostor: a square grid of views from the upper hemisphere, more of them near the horizon where a camera usually is, each view an albedo with alpha and a normal with depth. It rasterises on the CPU with a depth buffer, sampling each piece's baked maps for colour and cut-out, so CI gets the same bytes on every machine and no GPU is needed. It is a content-step crate, like the material graph engine.

ashlar-bevy draws an impostor as one camera-facing quad per instance with an ImpostorMaterial that finds the three frames nearest the view direction, blends them by their weights with a depth parallax offset, cuts out by alpha and hands the normal to Bevy's standard lighting, so an impostor is lit by the same sun as its prop.

The two meet in the prop file. ashlar-content's Content::write_impostor bakes a prop's nearest level in the materials its library gives it and answers the ImpostorRef its last level carries, and AshlarProp spawns that level as one more child: the quad, in the level's band, casting no shadow. ImpostorRef holds the layout whole rather than a copy of its fields, so the file and the renderer read one type.

What this does not do

  • Sorted alpha blending, and order-independent transparency.
  • Non-square bakes and a general atlas packer: impostor frames and leaf sheets are square grids, which need neither.
  • Skinned or vertex-animated props: wind is the game's shader, reading the vertex data above.
  • Generators. ashlar does not grow trees or shape rocks; a game does, and hands ashlar the meshes.

Consequences

  • A library, a building and a building file written before this ADR read and write unchanged. A prop file is a new kind with its own identifier.
  • ashlar-bevy gains a module and a material; the featureless game path stays free of the graph engine and the kernel. ashlar-impostor joins ashlar-manifold and ashlar-material as a tool-only crate.
  • ADR 0003's exclusion of transparency is narrowed: cut-out alpha and diffuse transmission are in, blending is still out.