// Procedural, tileable material textures. Purely cosmetic (client only). // Every texture is a TILEƗTILE RGB tile sampled in world coordinates, so patterns are seamless across chunks. export const TILE = 128; const MASK = TILE - 1; /** Integer hash in [0, 1). */ export function hash2(x: number, y: number, seed = 0): number { let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(seed, 1442695041); h = Math.imul(h ^ (h >>> 13), 1274126177); return ((h ^ (h >>> 16)) >>> 0) / 4294967296; } const smooth = (t: number): number => t * t * (3 - 2 * t); /** Periodic value noise: `cells` lattice cells across the tile. */ function valueNoise(x: number, y: number, cells: number, seed: number): number { const s = TILE / cells; const fx = x / s; const fy = y / s; const ix = Math.floor(fx); const iy = Math.floor(fy); const tx = smooth(fx - ix); const ty = smooth(fy - iy); const w = (i: number): number => ((i % cells) + cells) % cells; const a = hash2(w(ix), w(iy), seed); const b = hash2(w(ix + 1), w(iy), seed); const c = hash2(w(ix), w(iy + 1), seed); const d = hash2(w(ix + 1), w(iy + 1), seed); return a + (b - a) * tx + (c - a) * ty + (a - b - c + d) * tx * ty; } /** Periodic Worley noise: distance (in cell units) to the nearest jittered point, and that point's id. */ function worley(x: number, y: number, cells: number, seed: number): { d: number; id: number } { const s = TILE / cells; const fx = x / s; const fy = y / s; const ix = Math.floor(fx); const iy = Math.floor(fy); let best = Infinity; let id = 0; for (let oy = -1; oy <= 1; oy++) { for (let ox = -1; ox <= 1; ox++) { const cx = ix + ox; const cy = iy + oy; const wx = ((cx % cells) + cells) % cells; const wy = ((cy % cells) + cells) % cells; const px = cx + 0.15 + 0.7 * hash2(wx, wy, seed); const py = cy + 0.15 + 0.7 * hash2(wx, wy, seed + 1); const d = Math.hypot(px - fx, py - fy); if (d < best) { best = d; id = wx * 131 + wy; } } } return { d: best, id }; } /** Returns a brightness multiplier and an optional tint toward white (0..1) for a texture at tile coords. */ type Shader = (x: number, y: number) => [k: number, white: number]; const shaders: Record = { sponge: (x, y) => { const grain = (hash2(x, y, 3) - 0.5) * 0.08; const soft = (valueNoise(x, y, 8, 4) - 0.5) * 0.12; const { d } = worley(x, y, 16, 5); if (d < 0.22) return [0.68 + d, 0]; // air bubble if (d < 0.3) return [1.1, 0.05]; // bubble rim catches the light return [1 + grain + soft, 0]; }, 'soaked-sponge': (x, y) => { const [k] = shaders.sponge(x, y); const wet = valueNoise(x, y, 4, 9); return [k * 0.9, wet > 0.72 ? 0.18 : 0]; }, chocolate: (x, y) => { const u = x % 32; const v = y % 32; const n = (hash2(x, y, 7) - 0.5) * 0.04; if (u < 3 || v < 3) return [1.35 + n, 0.05]; // bevel highlight if (u > 28 || v > 28) return [0.7 + n, 0]; // bevel shadow const gloss = (u + v) % 64 > 40 && (u + v) % 64 < 46 ? 0.12 : 0; return [1 + n, gloss]; }, cream: (x, y) => { const n = valueNoise(x, y, 4, 11); const swirl = Math.sin((x + n * 40) / 9 + Math.sin(y / 11) * 2); return [0.9 + n * 0.1 - (swirl > 0.85 ? 0.08 : 0), 0]; }, jelly: (x, y) => { const deep = valueNoise(x, y, 4, 13); const { d } = worley(x, y, 8, 14); const stripe = (x - y + TILE * 4) % 64 < 6 ? 0.25 : 0; if (d > 0.12 && d < 0.17) return [1.15, 0.35]; // tiny bubble ring return [0.82 + deep * 0.25, stripe]; }, biscuit: (x, y) => { const grain = (hash2(x, y, 17) - 0.5) * 0.16; const row = Math.floor(y / 32); const hx = (x + (row % 2) * 16) % 32; const hy = y % 32; const dx = hx - 16; const dy = hy - 16; const r2 = dx * dx + dy * dy; if (r2 < 9) return [0.6, 0]; // docking hole if (r2 < 16 && dy > 0) return [1.15, 0.05]; return [1 + grain + (valueNoise(x, y, 8, 18) - 0.5) * 0.1, 0]; }, caramel: (x, y) => { const streak = valueNoise(x, y * 4, 4, 19); return [0.85 + streak * 0.3, streak > 0.8 ? 0.15 : 0]; }, wool: (x, y) => { const u = x % 10; const v = (y + (u < 5 ? u : 9 - u)) % 10; return [v < 2 ? 0.75 : 1 + (hash2(x, y, 21) - 0.5) * 0.1, 0]; }, plate: (x, y) => { const band = y % 14; if (band < 2) return [1.25, 0.3]; if (band === 13) return [0.85, 0]; return [1.05 + (valueNoise(x, y, 4, 23) - 0.5) * 0.06, 0.1]; }, }; const fallback: Shader = (x, y) => [0.95 + hash2(x, y, 1) * 0.1, 0]; /** Packed 0xRRGGBB tile for a material. */ export function makeTile(color: number, texture: string): Uint32Array { const shade = shaders[texture] ?? fallback; const r = color >> 16; const g = (color >> 8) & 0xff; const b = color & 0xff; const tile = new Uint32Array(TILE * TILE); for (let y = 0; y < TILE; y++) { for (let x = 0; x < TILE; x++) { const [k, white] = shade(x, y); const c = (v: number): number => Math.max(0, Math.min(255, Math.round(v * k * (1 - white) + 255 * white))); tile[y * TILE + x] = (c(r) << 16) | (c(g) << 8) | c(b); } } return tile; } /** Felt-pen outline thickness at a world pixel: 2..max, varying slowly along the line. */ export function makeThicknessTile(max: number): Uint8Array { const tile = new Uint8Array(TILE * TILE); for (let y = 0; y < TILE; y++) { for (let x = 0; x < TILE; x++) tile[y * TILE + x] = 2 + Math.round(valueNoise(x, y, 8, 31) * (max - 2)); } return tile; } export const tileIndex = (x: number, y: number): number => (y & MASK) * TILE + (x & MASK);