A texture is an image read on a surface: the boards of a wall, the bricks of a chimney, the bark of a maple. No texture is carried by the world. Every one is made where it is needed, from a handful of numbers (boards 0.3 m wide, a tile 1.8 m across, two greens, a seed) or from a function the world writes, and baked into the maps a material draws with. A world's surfaces are recipes, as its shapes are; a texture is a product of a material. This page covers the pattern recipes, the maps they make, how an image is laid on a shape (mapping), the cache, and how textures relate to images in general.

Words used on this page:

Texel
One cell of a texture.
Texture coordinates (UVs)
Two numbers, u and v, stored on each point of a mesh, saying where in the image that point lies; between points the renderer blends them, so an image is laid over the surface like a printed sheet.
Normal map
An image of small surface directions that gives a flat surface the shading of bumps: the grooves between boards catch the light.
ORM map
One image holding occlusion (how much surrounding light a crevice shuts out), roughness and metalness in its three color channels (the glTF packing).
Tile
A square of pattern that repeats seamlessly.

How you write it

A material painted by a pattern

part materials
  part cottageBoards: [physical(color: white, roughness: 1),
                       paintedBy(surfacepatterns.PlankParams.siding(512, seed: 1928))]    // board-and-batten
  part cottageRoof   : [physical(color: white, roughness: 1),
                       paintedBy(surfacepatterns.ShingleParams.cedar(512, seed: ...))]    // shingles
  part mapleBark     : [physical(color: white, roughness: 1),
                       paintedBy(surfacepatterns.BarkParams.redMaple(512, seed: 4101))]
  part granite       : [physical(color: white, roughness: 1),
                       paintedBy(surfacepatterns.RockParams.granite(512, seed: 6101))]   // the rocks'

paintedBy(...) is a modifier, a later item of the material's list after the constructor like every modifier, and it works on every kind of material: plain, shiny and physical alike. The image takes the place of the material's color (which is why the constructors above give white, "as the image is"); a pattern recipe's other maps (roughness, the normal map) are used where the kind has a use for them, which plain and shiny do not.

The calls are the package's own, written as it names them: a record type and one of its presets, PlankParams.siding(size, seed), BarkParams.redMaple(size, seed) and so on (the tables below). The package bakes the record to the maps of a material.

Changing a field of the record changes the pattern: surfacepatterns.PlankParams.siding(512, seed: 1928, boardWidth: 0.3 m, tileMeters: 1.8 m, colorA: srgb(72, 86, 66), colorB: srgb(48, 60, 46)). The package's fields are spelled with an American color.

A material painted by a world's own function

part materials
  part dolphins: [physical(color: white, roughness: 1),
                  paintedBy(surfacepatterns(tile: (3 m, 1.5 m), texel: dolphinsAt))]   // dolphinsAt is fn(u, v) texel

The labyrinth's frescoes are functions the world writes, handed to the same package (Open functions). The function's u and v are meters across and up the tile, and it returns a texel, made by texel(color: color, height: fraction = 0, roughness: fraction = 1, cavity: fraction = 1): a color, and the three values a tile's other maps are worked out from (below), which a flat painted color leaves at their defaults.

Painting an image from texture coordinates

Some images are not tiles: they are painted once over a whole shape, read through the shape's texture coordinates, as a balloon's gores are. painted makes such an image from a function of the coordinates:

painted(size: (texels, texels), colorAt: fn(uv: (number, number)) color,
        samples: count = 1, wraps: bool = false)
painted(size: (texels, texels), valueAt: fn(uv: (number, number)) number,
        samples: count = 1, wraps: bool = false)

Give it colorAt for a color image, or valueAt for a one-channel image of numbers (0 to 1), which is what a roughness map is. A material takes the second wherever it takes a number for roughness: physical(color: white, roughness: painted(size: ..., valueAt: glossAt)) (Materials: the kinds of material). The two images are laid on the shape by the same texture coordinates, so an amphora's black glaze (roughness 0.28) and bare clay (roughness 0.8) come from one function of the position, once for the color and once for the number. The island's amphora makes both in one pass over the same samples, and its renderer takes the roughness as the green channel of an ORM map beside the color image; the content states only the two images.

part balloon
  field gores: count = 24, common: true                      // a multiple of 4
  field panel: List[color] = [srgbFraction(0.98, 0.86, 0.06), srgbFraction(0.02, 0.60, 0.58),
                               srgbFraction(0.16, 0.34, 0.78), srgbFraction(0.13, 0.15, 0.40),
                               srgbFraction(0.02, 0.60, 0.58), srgbFraction(0.98, 0.86, 0.06)], common: true
                                                             // yellow, teal, blue, navy, teal, yellow
  fn tri(x: number) number { abs(2 * fract(x) - 1) }         // a triangle wave, 1 to 0 to 1

  fn chevronAt(uv: (number, number)) color {                // u round the envelope, v up from the mouth
    let u = uv.0;
    let v = uv.1;
    let g = floor(u * gores);                                // which gore
    let paint = if v < 0.3 { panel[g % 3] }                // plain gores at the foot ...
      else if v > 0.84 { panel[(g + 1) % 3] }                   // ... and at the crown
      else {                                                 // between, bands dipping to a V across every four gores
        let band = floor((v - 0.3) * 8 + 1.4 * tri(u * gores / 4 + 0.5));
        panel[[3, 4, 3, 5][band % 4]]
      };
    let onTape = fract(u * gores) < 1 / 16;                 // the load tape: a sixteenth of a gore
    if onTape { mix(black, paint, 0.75, in: srgb) } else { paint }   // darker: 0.75 of its sRGB numbers
  }

  part paints                                                // the balloon's own materials
    part envelopePaint: [shiny(white, highlight: 0.12, shininess: 12),
                         paintedBy(painted(size: (gores * 32 texels, 1024 texels), colorAt: chevronAt, samples: 2))]

The chevron balloon's paint: 24 gores of 32 texels each, 768 by 1024 texels, each texel the mean of 2 by 2 samples. The six paints are written as the sRGB fractions of the balloon's paint table, so the image's numbers are exactly those (Values: colors). Each gore takes one of the six paints by its number; at the foot and the crown they run plain, and between, bands of paint dip to a V across every four gores, cycling through four of the paints. A sixteenth of each gore's width is the load tape, drawn darker: 0.75 of the paint's sRGB numbers, about 0.53 of its light. The envelope's shape is a lathe, whose texture coordinates run once round (u) and once up the outline by length (v), so the image wraps the envelope exactly once. The paint is held in the balloon's own frameless holder, paints, as the balloon names it everywhere (The balloon ride).

Mapping

part cottage
  part walls  : boardWalls(),   material: materials.cottageBoards     // mapped by face, in meters
  part chimney: chimneyStack(), material: materials.chimneyBrick      // mapped onto one sheet round the corners

Where a material's image lies on a shape is the shape's texture coordinates, which belong to the shape: a drawn shape gets its texture coordinates from its generator (round and along a lathe, a sweep or a loft); a primitive gets its natural ones (a box 0 to 1 across each face, a sphere round and pole to pole, a cylinder round and along: Appendix: shape tools, primitives); and the projection tools change them: uvPlanar, uvBox, uvCylindrical, uvSpherical and the general uvAt, which works each point's coordinates out from its place (Appendix: shape tools, "Texture coordinates"). The material says what the image is; the shape says how it lies.

fn boardWalls() shape {
  let walls = ...;                                  // the walls, built from boxes
  uvBox(walls, size: 1.8 m)                         // one repeat of the boards' tile every 1.8 m, on every face
}

What it means

The pattern recipes: surfacepatterns

The package makes procedural patterns that fill a tile with a recognizable surface (boards, shingles, flagstones, stone, bark, brick) and produce the maps of a physically based material for it. Each pattern has a parameter record (sizes in meters, linear colors, a seed), presets taking a size and a seed, a texel function, and a bake:

PatternRecord's fieldsPresets
Planks: PlankParamssize, tileMeters, seed, boardWidth, boardLength, battenWidth, battenRaise, gap, colorA, colorB, variation, grain, weathersiding(size, seed) (board-and-batten, 1.6 m tile), decking(size, seed)
Shingles: ShingleParamstile, shingle width, exposure, thickness, gap, colors, variation, weathercedar(size, seed)
Flagstones: FlagstoneParamsstone size, jitter, gap, bevel, unevenness, colors, lichen, soil and growth in the gapsgarden(size, seed)
Rock: RockParamssize, tileMeters, seed, blockSize, relief, colorA, colorB, lichen, lichenAmount, stratagranite(size, seed): tile 2.1 m, blocks 0.7 m, relief 0.12, colors (176, 168, 156) and (70, 68, 68), lichen (150, 156, 118) at 0.3, strata 0.4
Edging stones: EdgingStoneParamsstone size, jitter, gap, height, colors, lichen, soilfieldstone(size, seed). The island's edging stones paint the granite recipe instead, with colorA: (148, 142, 130), colorB: (92, 84, 76) and lichenAmount: 0.4
Bark: BarkParamssize, tileMeters, seed, furrowWidth, furrowDepth, meander, colorA, colorB, furrow, lichen, lichenAmount; the tile wraps round the trunkredMaple, whitePine, pine, each (size, seed); the table below
Brick: BrickParamsbrick length and height, mortar, recess, arris, four shades, an occasional dark brick, mortar colorstandard(size, seed) (running bond)
Needle tufts: NeedleTuftParamsa cut-out card image of a conifer's needles, with an alpha channel (RGBA): kind (TUFT_PINE, TUFT_HEMLOCK, TUFT_FIR, TUFT_SPRIG, TUFT_ARAUCARIA), fascicles, needle length and width, colors, twigs, coneswhitePine, hemlock, balsamFir, whitePineCrown, spruce, denseFir, norfolk, each (size, seed)

The bark presets (colors in sRGB 0 to 255; furrow in meters; tile 1.2 m for all three):

PresetfurrowWidth, furrowDepth, meandercolorA, colorB, furrowlichen, lichenAmount
BarkParams.redMaple0.04, 0.014, 0.8(120, 108, 98), (84, 74, 68), (36, 30, 28)(130, 142, 112), 0.45
BarkParams.whitePine0.06, 0.024, 0.55(128, 118, 108), (94, 86, 78), (36, 32, 30)(130, 140, 118), 0.2
BarkParams.pine0.08, 0.035, 0.6(140, 88, 60), (100, 62, 44), (34, 24, 20)(120, 130, 90), 0.12

The island's conifer needles are NeedleTuftParams.spruce(512, seed: 11), NeedleTuftParams.denseFir(512, seed: 12) and NeedleTuftParams.whitePineCrown(256, seed: 13), each baked to an image with an alpha channel that a material's cutOut reads. spruce is a sprig kind with 20 needles to a fascicle, each 0.062 m long and 0.0058 m wide, from base color (14, 40, 30) to tip (44, 84, 58), with 15 side twigs and bright new growth (128, 160, 60) over 0.8 of the tips; denseFir is longer (0.075 m) and slimmer (0.0055 m) with 17 side twigs and denser growth. Bark: the island paints the red maple with BarkParams.redMaple(512, seed: 4101) and the pine with BarkParams.pine(512, seed: 4202), the pine's color multiplied by a part tint of (176, 164, 156) over 255.

Every pattern is a function of a point on its tile: <x>Texel(params, u, v) returns a PatternTexel, a height (0 to 1 of the tile's relief), a color, a roughness and a cavity, "1 open, lower shut", for occlusion a horizon search would miss, such as a mortar joint. fillTexels runs the function over the tile, one row per pass across every core, and finishField works out the rest:

Patterns repeat every 1 in u and v and fit a whole number of features; a 1024 tile is a texel of about 2 mm on a 2 m tile. Every random number is a hash of the seed and a lattice point, so the same maps come out on every run. The package's own noise (fbmNoise, ridgedNoise, voronoi) is there for texel functions to call.

The material parameters: pbrmaterial

Underneath, pbrmaterial is what goes into a physically based material, as opposed to how it is lit: fractal noise layers (fBm, ridged, billow) that tile, per-texel functions for height, normal, convexity and occlusion, a surface whose color and roughness ramp with height, edge wear driven by convexity and grime by cavity, and a bake into a texture set: base color, ORM, normal, height, wear and grime (bakeTextureSet). It is pure and hashed; there is no stream and no platform RNG. Not built: layering, a material graph, parameter fields evaluated in shaders (a film's thickness, a wet mask), GPU bakes; render has no mipmaps (smaller copies of a texture for far surfaces), so a tiled ground shimmers in the distance.

Images painted from texture coordinates

Besides tiles, images can be painted straight from a shape's texture coordinates: a balloon's chevrons (32 texels across each of its 24 gores) and its wicker, an amphora's bands (2048 by 2048, about a millimeter a texel), a maple's leaf palette (a strip read by a coordinate the tree generator sets from each leaf's exposure, so one material shades a crown from green-yellow inside to scarlet outside), a person's skin and eyes. Each is a recipe run at build, like a tile. A world paints its own with painted (above).

Mapping: the shape's, read by the material

The texture coordinates belong to the shape (above), but only mean something with the material: they state, in the shape, how big the material's pattern is on this surface. A brick must be a brick's size on every face. Several methods are in use, each chosen to suit its shape, and each is one of the three sources: the generator's own, a primitive's natural ones, or a projection tool:

A face-by-face mapping in meters is uvBox(s, size: ...), and a flat or wrapped one is uvPlanar, uvCylindrical or uvSpherical (Appendix: shape tools, "Texture coordinates"). An unwrapping tool, which cuts a surface into flat pieces laid on one sheet, is not among them.

Where and when textures are made

Textures and images

A texture is one use of the image, the 2D half's unifying type: a rectangle of pixels with a width and a height (Images). A pattern bake is a source that makes an image once; a camera drawn into a texture (a mirror) is a source that makes a fresh image as its inputs change (Cameras and views); a layout rendered to pixels is another (a sign). A material reads images; where they come from is the image's business.

Files are the escape hatch, recipes the default. A file a part holds (a photograph) is stored by its content's hash. Whether a world may carry an image at all, and within what size, is open.

How it connects

Open questions

  1. Where a mapping's scale lives: the cottage's 1.8 m tile is written in the shape's projection (uvBox(walls, size: 1.8 m)) and again in the material's bake; one of the two could read the other.
  2. May a world carry an image? Never; yes, as data, for what no recipe makes; or within a size the browser sets.
  3. Pattern vocabulary: which records and presets are vocabulary names, and how a world's own texel function is admitted beside them.
  4. A tile's size in two places: a tiling painted image is repeated by the shape's projection (uvBox(walls, size: 0.4 m)) and drawn at so many texels a tile; as with the cottage's boards, one could read the other.
  5. Exactness: a texture need only look the same on every machine; whether content states that per product.
  6. Weathering: the patterns carry wear in their color; pbrmaterial computes wear and grime masks. Whether wetness and seasons change a texture's inputs while the world runs.

← Materials | Contents | Lights →