Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

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.