Builtin Library
readme.md
surfaceDetail system
Two texture sets on one surface, and a sampling mode that hides the fact that either of them tiles.
A tiled texture repeats on a regular grid, and on any surface large enough to show several repeats that grid is the first thing that reads as artificial — the same pebble appearing every two metres in a perfect lattice. Meanwhile the opposite problem shows up close: a base texture authored to cover a whole hillside has no resolution left when the camera stands on it.
Both are addressed here, and neither needs a bigger texture.
Using it
local surfaceDetail = require("@builtin::systems.surfaceDetail.surfaceDetail")
local ground = surfaceDetail.material("cliff_ground", {
texture = "rock_albedo",
normalTexture = "rock_normal",
tiling = 12,
-- Sample through a randomised lattice instead of straight UVs.
stochastic = true,
-- A finer grain that only shows up close.
detailNormalTexture = "grain_normal",
detailTiling = 96,
detailFadeDistance = 25,
})
entity(id).component.add("Model", { model = "plane", material = ground })
Stochastic tiling
Each fragment lands in a cell of a triangular lattice laid over the UVs. The three corners of its triangle each carry a random UV offset, so the surface is sampled three times from three different places in the texture and blended by the fragment's barycentric position in the cell. Neighbouring fragments share corners, so the result is continuous, while the periodicity is gone.
Blending three samples would ordinarily flatten contrast, because averaging independent samples shrinks variance. The blend re-centres on the texture's mean and rescales, so the surface keeps the source texture's contrast rather than trading a repetition grid for a washed-out one.
It costs three texture reads per map instead of one. stochastic = false (the
default) takes the single-sample path.
The detail layer
detailTiling is independent of tiling, so the detail set can repeat far more
often than the base — the base carries what the surface is, the detail carries
the grain you only see standing on it. detailFadeDistance fades the layer out
with distance so it does not alias into noise across a far surface; omit it and
the detail holds at every distance.
Base and detail normals combine with a reoriented blend, which rotates the detail onto the basis the base normal describes rather than averaging the two. A flat detail normal returns the base exactly, so a material carries the detail path at no cost until it supplies one.
Authoring the material directly
surfaceDetail.shader() returns the shader identity, for building the material
by hand with asset.create("material", …). Its properties are uv_scale,
detail_uv_scale, stochastic, detail_normal_scale, detail_color_weight
and detail_fade_distance, alongside the usual metallic-roughness set.