Builtin Library
README.md
layeredMaterial
Compiles a stack of materials into one WGSL surface shader.
local layeredMaterial = require("@builtin::systems.layeredMaterial.layeredMaterial")
| Call | What it does |
|---|---|
read(materialRef) | Reads a layer's inputs off a material that already exists: { material, shader, values, textures, unsupported }. |
compile(stack, name?) | Returns { name, wgsl, properties, layers, textureSlots, values, textures, unsupported } — the two files a .shader asset holds, plus what a material wearing it has to be given. |
check(stack) | compile with the error handed back: (true, result) or (false, message). |
install(stack, opts?) | Writes the compiled stack out as a real .shader asset. opts.name names it; opts.into places it as it is created. |
material(stack, opts?) | Installs, then makes a material wearing the shader and writes every property the stack declares onto it, from the stack where it says one and from the declared default where it does not. opts.values is written last, over them. |
property(index, input) | The property name a layer's input is declared under — l<index>_<input>, counting the bottom layer as 1. |
inputs() | The vocabulary: { values, textures, coverage, mask, surface }. |
limits() | { maxLayers, textureSlots }. |
A stack
{
name = "worn_barrel",
layers = {
{ material = "@builtin::materials.default" },
{
material = "@builtin::materials.gold",
blend = 0.6,
mask = "@builtin::systems.terrainSystem.demo.textures.rock_diff",
maskChannel = "r", -- or a { x, y, z, w } selector
maskContrast = 2,
instanceLane = 0,
instanceComponent = 0,
uvScale = { 4, 4 },
uvOffset = { 0, 0 },
},
},
}
The bottom layer is first, and the coverage controls belong to a layer laid over
it: writing one on the bottom layer raises. A layer names a material, or writes
its inputs directly as values and textures, or does both — written values win
over the material's, which is how a stack lays a material over another and still
says "but rougher here". A layer read off a material takes the maps that material
binds; a slot it leaves on a built-in fallback (default:white) is a slot it
binds nothing to.
Coverage
A layer past the first is revealed by the product of three terms:
| Term | Property |
|---|---|
| Mask texture, one channel of it, shaped by a contrast | l<i>_mask_texture, l<i>_mask_channel, l<i>_mask_contrast |
| Per-material amount | l<i>_coverage |
| Per-instance amount, one lane of the entity's shader data | l<i>_instance_lane, l<i>_instance_component |
renderer.instanceData.set(entity, lane, amount) writes that lane, one entity at
a time, so one stack over many entities still reveals its top layer by a
different amount on each.
A layer written without a mask declares neither mask property nor mask slot, so the mask term is absent from the shader rather than neutral in it.
Bounds
limits().maxLayers layers, limits().textureSlots texture slots between them.
Slots are spent on the maps the layers actually bind: a layer of uniform values
alone spends none. A stack past the budget is refused, naming what each layer
spent.
Shading
The layers are blended as shading inputs and shaded once. A fully covered layer therefore renders as that layer alone, and a half-covered surface as half of each.