Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

readme.md

layeredMaterial

A surface written as a stack of materials that already exist, compiled to one WGSL surface shader.

Rust over painted metal, snow over rock, mud over a vehicle, lacquer over paint: each is one material laid over another. Both materials are usually already authored; what a stack adds is a way to say "this one, over that one, where the mask says", and to move the "where" at runtime.

local layeredMaterial = require("@builtin::systems.layeredMaterial.layeredMaterial")

local mat = layeredMaterial.material({
    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",
            maskContrast = 2,
            uvScale = { 4, 4 },
        },
    },
})

entity.spawn("barrel").component.add("Model", { model = "cube", material = mat })

material compiles the stack, writes it out as a real .shader asset, makes a material wearing it, and writes every property the stack declares onto it — from the stack where it says one, from the declared default where it does not — plus every texture the layers bind. Calling it again with the same names is how a stack is iterated on: what the second call says is what the material holds.

Driving the blend

Every coverage term is a property of the generated material, so a running scene moves it:

local ref = asset.ref("worn_barrel", "material")
ref:setProperty(layeredMaterial.property(2, "coverage"), 0.9)

property(index, input) names what the stack declared — layer 1 is the bottom one. The per-instance term is the other half of the same knob: point a layer at one lane of the entity's own shader data and each entity carries its own amount.

{ material = "@builtin::materials.rust", blend = 1, instanceLane = 0, instanceComponent = 0 }

What a stack costs

BoundValueWhy
Layerslimits().maxLayersThe shading cost of a stack is bounded before it is compiled.
Texture slotslimits().textureSlotsWebGPU guarantees 16 sampled textures per fragment stage and the renderer's own bind group holds several of them.

Slots are spent on the maps the layers actually bind. A layer whose material is uniform values alone costs none; one binding all five glTF maps costs five; a mask costs one more. A stack past the budget is refused, naming what each layer spent.

Energy

The layers are blended as shading inputs and shaded once. Shading each layer separately and adding the results would put more light back out than arrived; a surface half covered by a layer here puts out what half of each layer would, and a fully covered one puts out exactly what the covering layer alone would.

The layer vocabulary

inputs() lists it. A layer carries the metallic-roughness set, the dielectric parameters, emissive, clearcoat, anisotropy, transmission, thickness and attenuation, plus a UV transform of its own so an overlay tiles at its own texel density. Normal scale and occlusion strength are declared by a layer that binds the map they modulate. Alpha and sidedness belong to the surface and are read off the bottom layer. A property a layer's source material sets that a layer does not carry comes back in unsupported.

A layer binding no normal map is flat — its shading normal is the geometric one. The mesh's vertex colour tints the bottom layer, the one the geometry wears.

Reading a material first

local read = layeredMaterial.read("@builtin::materials.gold")
print(read.shader, read.values.metallic, read.textures.base_color_texture)

read is what compile does with a { material = ... } layer, on its own.

Where this sits

@builtin::shaders.pbr_layered is a hand-written two-layer shader: base and top, fixed at two, with the top layer's maps chosen when it was written. A stack says the same thing without the shader being written for the pair, and holds more than two layers as long as their maps fit the budget.

@builtin::systems.materialGraph compiles a node graph to the same WGSL surface DSL. A graph describes one material's arithmetic; a stack describes how whole materials sit on each other.