| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- // 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<string, Shader> = {
- 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);
|