Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

layeredMaterial

Compiles a stack of materials into one WGSL surface shader.

local layeredMaterial = require("@builtin::systems.layeredMaterial.layeredMaterial")
CallWhat 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:

TermProperty
Mask texture, one channel of it, shaped by a contrastl<i>_mask_texture, l<i>_mask_channel, l<i>_mask_contrast
Per-material amountl<i>_coverage
Per-instance amount, one lane of the entity's shader datal<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.