Procedural Material Textures
Source: room-at-dawn (MIT), from src/core/tex.js, rng.js, src/scene/materials.js, and the material functions under parts/.
The original project's bedroom does not use a single image file; all textures are generated procedurally at load time.
Core ideas:
- Design in physical units. Heights are in millimeters; each tile is clearly defined in meters, and UVs are also in meters. This way
repeat = 1 / tileMeters, and changing resolution does not alter the appearance. - Most realism comes from roughness, not color. Fingerprints, wipe marks, and dust are nearly invisible in the color channel; they only show up when highlights scan across the surface.
- All textures are seamless and reproducible. Noise functions have integer periods; randomness always uses
RNG(seed)so the same seed always produces identical results (essential for light baking and screenshot regression testing).
Files
| Path | Contents |
|---|---|
scripts/tex.js | Foundations: RNG, pnoise/pfbm (independent period per axis), worley, vn (lookup-table fast noise), field, normalBytes (in millimeters), dataTex/canvasTex/packTex/colorTex, avgColor, once (caching), rep (share images, only change repeat), seamScore, texData |
scripts/surfaces.js | wallNormal + wallPatch (chip wallpaper with 3.2m-scale plaster & paint variation), lacquerNormal, smudgeRough, dustRough, floorMaps, woodMaps, oakVeneer |
scripts/textiles.js | weaveNormal, waffleNormal, terryNormal, ribNormal, knitNormal, feltNormal, ropeNormal, fabricMaterial (sheen with backlight translucency) |
scripts/wear.js | wornMaps (a whole used surface: both color and roughness), speckleTexture, scuffTexture, and basic brushes softBlob, strand, fingerprint, noiseTile, overlay |
scripts/print.js | word/phrase (fake words from syllables), PAL, packer, fitText, paperGrain, makeBook + bookAtlas + bookGeometry, pagesTexture, topoPoster, photoPrints |
example/ | A room corner, each surface uses a material from the library. ?cam= (room, desk, glint, fabric, floor, wall, books); ?flat=1 switches to solid colors for comparison |
example/preview.jpg | Example screenshot |
references/recipes.md | Recipes for each material: dimensions, parameters, and how to add new ones |
references/verify-engines.md | Verification methods, known issues, performance, and porting to Godot/Unreal |
Running the Example
cd <skill folder> && python3 -m http.server 5422 --bind 127.0.0.1
# Open http://127.0.0.1:5422/example/ (window.__tex shows generation time, seam score, and average albedo for each texture)
(<skill folder> is the Base directory shown when loading this skill; when installed as a plugin, it is located under ~/.claude/plugins/cache/)
Usage
import { rep } from './scripts/tex.js';
import { floorMaps, wallNormal, wallPatch, lacquerNormal } from './scripts/surfaces.js';
import { knitNormal, fabricMaterial } from './scripts/textiles.js';
import { wornMaps } from './scripts/wear.js';
const fl = floorMaps(); // UV in meters; repeat is already set
floor.material = new THREE.MeshStandardMaterial({ map: fl.map, normalMap: fl.normalMap, roughnessMap: fl.roughnessMap, roughness: 1 });
wall.material = wallPatch(new THREE.MeshStandardMaterial({ color: 0xebebe6, roughness: 0.93, normalMap: wallNormal() }));
door.material = new THREE.MeshStandardMaterial({ color: 0xdddddd, roughness: 0.6, normalMap: rep(lacquerNormal(), 9), normalScale: new THREE.Vector2(0.18, 0.18) });
sweater.material = fabricMaterial(0x6f7f6a, { normal: knitNormal(), tileM: 0.1 });
const w = wornMaps({ sizeM: [1.2, 0.6], hands: [[0.55, 0.5]], rings: [[0.9, 0.4, 0.032, 0.6]], prints: 6 });
deskTop.material = new THREE.MeshStandardMaterial({ map: w.map, roughnessMap: w.roughnessMap, roughness: 1 });
- For materials that have a
roughnessMap, setroughnessto 1. three.js multiplies the two together. packTexandwornMapsstore roughness in the G channel; grayscale canvases can also be used directly.- Every material function returns
avg(the linear average color) for use as the albedo inthreejs-baked-gi.
Must Follow
- Do not use
Math.random(). Every texture must have a fixed seed. - Noise periods must be integers and aligned with the tile grid (e.g.,
pfbm(u*6, v*6, oct, seed, 6)). Misalignment causes seams. UseseamScoreto check. - Heights are in millimeters; use
normalBytes(H, w, h, mmPerTile). Changing resolution does not change the bump strength; some textures have non-square tiles (e.g., floor 1.1 × 1.9 mm), so supply the two axes separately. - Draw roughness together with color. Each stroke in
wornMapsis painted on both canvases using the samenew RNG(i), so the shapes align. - Dark surfaces should have fewer specks and highlights (e.g., the dark desktop reduces 9000 to 2500), otherwise they look like granite.
- All text and patterns drawn on canvases must be self-composed (
word(), geometric shapes); do not use real book titles, brands, or trademarks. - The largest textures (2048²) take 0.1–0.35 seconds to generate; cache them with
onceand share viarep(). Do not generate one per object.
Benchmarks (RTX 4090, Chrome headless, total scene 1.1s)
| Texture | Size | Time | Seam Score (x/y) |
|---|---|---|---|
floorMaps | 2048² × 3 maps | 0.32 s | color 0.96 / 1.10, normal 1.00 / 1.02, roughness 0.89 / 1.05 |
woodMaps | 1024² × 3 | 0.20–0.23 s | color 0.93 / 1.12, normal 1.30 / 1.06 |
wornMaps (1.2 × 0.6 m, 3400 px/m) | 4080×2040 + half-size roughness | 0.13 s | non-repeating |
oakVeneer | 1024² × 3 | 0.10 s | 0.98–1.06 |
wallNormal | 1024² | 0.04 s | 1.00 / 1.07 |
lacquerNormal, smudgeRough | 512² | 0.02–0.04 s | 0.98–1.04 |
| 7 fabric types | 128²–512² | 1–19 ms | 0.94–1.10 (rope y is 2.03, intentional groove; see verify-engines.md) |
bookAtlas (12 books) | 2048×1536 | 0.05–0.08 s | — |
A seam score close to 1 means seams are invisible; over 2 requires inspection. The calculation adds 1 color step to both numerator and denominator to avoid large ratios from sub-1-step noise in near-flat directions (e.g., grain-following wood).