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.
sizeis the image's width and height, each in texels, stated by the author, because the quality depends on it: the balloon's chevron edges are crisp at 32 texels a gore.colorAtis called for each sample withuv, the two texture coordinates, each from 0 to 1 across the image (u across, v up), and returns the color there. It is an open function: pure, bounded and deterministic. As a shape may, it reads any fixed field of the part it is written in, own or common, and the world's fixed fields, but never a mutable one (Construction: what a shape reads): the balloon'schevronAtreads itsgoresandpanel. A function that runs at build may therefore read a table computed once, if the table is a fixed field: it is worked out at build, in dependency order, before the image is painted. The amphora's profile rows (each band's radius, height and paint) are one such field,profile, and itscolorAtandvalueAtboth look a row up in it.samplesis supersampling, per axis: withsamples: 1each texel is sampled once, at its middle; withsamples: 2it is the mean of 2 by 2 samples, evenly spaced inside it (at a quarter and three quarters of the way across, and up), so an edge that crosses a texel is blended, not stepped. The samples are averaged on their sRGB numbers, as a paint program averages, and the image is stored as sRGB.wrapssays how the image is read past its edges.wraps: false, the default, paints it once over the whole shape.wraps: truemakes a tiling image: it repeats, every 1 in u and in v, as far as the shape's texture coordinates go, so its right edge must meet its left and its top its bottom. The basket's wicker is one 256 by 256 tile, read throughuvBox(walls, size: 0.4 m), so it repeats every 40 cm on every face.- The image is what
paintedBylays on the material.
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:
| Pattern | Record's fields | Presets |
|---|---|---|
Planks: PlankParams | size, tileMeters, seed, boardWidth, boardLength, battenWidth, battenRaise, gap, colorA, colorB, variation, grain, weather | siding(size, seed) (board-and-batten, 1.6 m tile), decking(size, seed) |
Shingles: ShingleParams | tile, shingle width, exposure, thickness, gap, colors, variation, weather | cedar(size, seed) |
Flagstones: FlagstoneParams | stone size, jitter, gap, bevel, unevenness, colors, lichen, soil and growth in the gaps | garden(size, seed) |
Rock: RockParams | size, tileMeters, seed, blockSize, relief, colorA, colorB, lichen, lichenAmount, strata | granite(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: EdgingStoneParams | stone size, jitter, gap, height, colors, lichen, soil | fieldstone(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: BarkParams | size, tileMeters, seed, furrowWidth, furrowDepth, meander, colorA, colorB, furrow, lichen, lichenAmount; the tile wraps round the trunk | redMaple, whitePine, pine, each (size, seed); the table below |
Brick: BrickParams | brick length and height, mortar, recess, arris, four shades, an occasional dark brick, mortar color | standard(size, seed) (running bond) |
Needle tufts: NeedleTuftParams | a 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, cones | whitePine, 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):
| Preset | furrowWidth, furrowDepth, meander | colorA, colorB, furrow | lichen, lichenAmount |
|---|---|---|---|
BarkParams.redMaple | 0.04, 0.014, 0.8 | (120, 108, 98), (84, 74, 68), (36, 30, 28) | (130, 142, 112), 0.45 |
BarkParams.whitePine | 0.06, 0.024, 0.55 | (128, 118, 108), (94, 86, 78), (36, 32, 30) | (130, 140, 118), 0.2 |
BarkParams.pine | 0.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:
- a base color, in sRGB;
- an ORM map whose occlusion is a horizon search over the heights times the pattern's cavity, whose roughness is the pattern's, and whose metalness is 0;
- a normal map from the heights;
- an 8-bit height map and the full-precision heights;
- wear and grime masks, all zero: "these patterns carry their weathering in their color".
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:
- By face, in meters (the cottage): each flat face's points get their place across the face and up its slope in meters, over the tile's size, so boards are 0.3 m wide on every wall and repeat every 1.8 m.
- By the generator: u round, v along, each 0 to 1 by length, recorded by
lathe,sweepandloftas theuvchannel; triangulation splits a point where two faces give it different coordinates, so an image wraps once without smearing back across the last face (Sculpting). - By box onto one sheet (the chimney): all four faces read one sheet 4.05 m wide at 2.5 mm a texel, so the brick courses run unbroken round the corners; the stack is sized in whole bricks (4 by 3 in plan, 45 courses) so the bond closes. Pattern, shape and mapping are designed together; none can be changed alone.
- By box projection onto the nearest plane (the rocks):
noiserockgives each triangle coordinates from the plane most nearly facing it, a tile everytileMeters, so a granite tile runs at its own scale whatever the rock's size.
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
- At build, on each machine. Neither meshes nor images cross the network. A texture is a product of a material, made where it is needed.
- Cached, never content. A baked texture set is kept on disk under a key made of the browser program, the bake's name, its tile size and seed and a format number. It holds the three maps the world draws with (color, normal, ORM). For content the key would be the description's own hash and the vocabulary's version.
- Only where drawn. A texture is needed only by a machine that draws; a server needs none.
- Cost stated as a requirement, or as a count. For a pattern tile, "a 3 m panel with detail down to about 3 mm" is the requirement and 1024 texels the browser's choice. An image painted over a whole shape states its count (
size: (768 texels, 1024 texels)), as a shape states its steps; a requirement form is later sugar over the count.
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
- Materials: what holds the maps.
- Open functions: texel functions a world writes.
- Sculpting: the
uvchannel; Appendix: shape tools: the projection tools. - Images: the image type and its sources.
- Loading: the bake cache; products per machine.
Open questions
- 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. - May a world carry an image? Never; yes, as data, for what no recipe makes; or within a size the browser sets.
- Pattern vocabulary: which records and presets are vocabulary names, and how a world's own texel function is admitted beside them.
- A tile's size in two places: a tiling
paintedimage 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. - Exactness: a texture need only look the same on every machine; whether content states that per product.
- 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.