Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit65083fe8f2

README.md

pbr_layered

A surface composed from two complete PBR layers: a base, and a top layer laid over it wherever coverage says so.

Rust over painted metal, snow over rock, mud over a vehicle, wear over a finish. Pre-blending those into one texture set bakes the coverage in — it can no longer change at runtime, and every entity wearing the material wears it identically. Here both layers keep their own shading inputs and coverage is evaluated per fragment, so the same material can be pristine on one entity and ruined on the next.

Coverage

Coverage is the product of three terms, each neutral by default:

termpropertywhat it is for
masktop_mask_texture, top_mask_channel, top_mask_contrastwhere the top layer gathers — creases, edges, upward faces
materialtop_coveragehow far the look has progressed, for every entity sharing the material
instancetop_instance_lane, top_instance_componenthow far for this entity

top_coverage defaults to 0, so a material that sets nothing renders as its base layer alone.

top_mask_contrast shapes the mask: 1 is a linear ramp, higher values pull the top layer back into the mask's brightest areas, which is what makes rust and snow gather in creases instead of washing evenly over everything.

Driving it per instance

Point the material at an instanceData lane, then write that lane per entity:

-- material: top_instance_lane = 0, top_instance_component = 0
local clean, rusted = entity.find("barrel_a"), entity.find("barrel_b")
if clean and rusted then
    renderer.instanceData.set(clean, 0, 0.1)    -- nearly clean
    renderer.instanceData.set(rusted, 0, 0.9)   -- heavily rusted
end

Both entities share one material and one draw path; only the lane differs.

Texture slot budget

WebGPU guarantees 16 sampled textures per fragment stage and the engine's group(0) already holds several. This shader declares 9, which leaves room for those plus a margin. Two full five-map layers would not fit, so the top layer carries the three maps that make it read as a different material — albedo, roughness/metallic, normal — and takes occlusion and emissive from the base.

Layer count is two. The cost is fixed and predictable: both layers are always sampled, and coverage selects between their inputs rather than branching.

Why the inputs blend rather than the results

Lighting each layer separately and adding puts more energy back out than arrived, so a half-covered surface would read brighter than either layer alone. Interpolating the shading inputs and lighting once cannot do that.