textures.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. // Procedural, tileable material textures. Purely cosmetic (client only).
  2. // Every texture is a TILE×TILE RGB tile sampled in world coordinates, so patterns are seamless across chunks.
  3. export const TILE = 128;
  4. const MASK = TILE - 1;
  5. /** Integer hash in [0, 1). */
  6. export function hash2(x: number, y: number, seed = 0): number {
  7. let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(seed, 1442695041);
  8. h = Math.imul(h ^ (h >>> 13), 1274126177);
  9. return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
  10. }
  11. const smooth = (t: number): number => t * t * (3 - 2 * t);
  12. /** Periodic value noise: `cells` lattice cells across the tile. */
  13. function valueNoise(x: number, y: number, cells: number, seed: number): number {
  14. const s = TILE / cells;
  15. const fx = x / s;
  16. const fy = y / s;
  17. const ix = Math.floor(fx);
  18. const iy = Math.floor(fy);
  19. const tx = smooth(fx - ix);
  20. const ty = smooth(fy - iy);
  21. const w = (i: number): number => ((i % cells) + cells) % cells;
  22. const a = hash2(w(ix), w(iy), seed);
  23. const b = hash2(w(ix + 1), w(iy), seed);
  24. const c = hash2(w(ix), w(iy + 1), seed);
  25. const d = hash2(w(ix + 1), w(iy + 1), seed);
  26. return a + (b - a) * tx + (c - a) * ty + (a - b - c + d) * tx * ty;
  27. }
  28. /** Periodic Worley noise: distance (in cell units) to the nearest jittered point, and that point's id. */
  29. function worley(x: number, y: number, cells: number, seed: number): { d: number; id: number } {
  30. const s = TILE / cells;
  31. const fx = x / s;
  32. const fy = y / s;
  33. const ix = Math.floor(fx);
  34. const iy = Math.floor(fy);
  35. let best = Infinity;
  36. let id = 0;
  37. for (let oy = -1; oy <= 1; oy++) {
  38. for (let ox = -1; ox <= 1; ox++) {
  39. const cx = ix + ox;
  40. const cy = iy + oy;
  41. const wx = ((cx % cells) + cells) % cells;
  42. const wy = ((cy % cells) + cells) % cells;
  43. const px = cx + 0.15 + 0.7 * hash2(wx, wy, seed);
  44. const py = cy + 0.15 + 0.7 * hash2(wx, wy, seed + 1);
  45. const d = Math.hypot(px - fx, py - fy);
  46. if (d < best) {
  47. best = d;
  48. id = wx * 131 + wy;
  49. }
  50. }
  51. }
  52. return { d: best, id };
  53. }
  54. /** Returns a brightness multiplier and an optional tint toward white (0..1) for a texture at tile coords. */
  55. type Shader = (x: number, y: number) => [k: number, white: number];
  56. const shaders: Record<string, Shader> = {
  57. sponge: (x, y) => {
  58. const grain = (hash2(x, y, 3) - 0.5) * 0.08;
  59. const soft = (valueNoise(x, y, 8, 4) - 0.5) * 0.12;
  60. const { d } = worley(x, y, 16, 5);
  61. if (d < 0.22) return [0.68 + d, 0]; // air bubble
  62. if (d < 0.3) return [1.1, 0.05]; // bubble rim catches the light
  63. return [1 + grain + soft, 0];
  64. },
  65. 'soaked-sponge': (x, y) => {
  66. const [k] = shaders.sponge(x, y);
  67. const wet = valueNoise(x, y, 4, 9);
  68. return [k * 0.9, wet > 0.72 ? 0.18 : 0];
  69. },
  70. chocolate: (x, y) => {
  71. const u = x % 32;
  72. const v = y % 32;
  73. const n = (hash2(x, y, 7) - 0.5) * 0.04;
  74. if (u < 3 || v < 3) return [1.35 + n, 0.05]; // bevel highlight
  75. if (u > 28 || v > 28) return [0.7 + n, 0]; // bevel shadow
  76. const gloss = (u + v) % 64 > 40 && (u + v) % 64 < 46 ? 0.12 : 0;
  77. return [1 + n, gloss];
  78. },
  79. cream: (x, y) => {
  80. const n = valueNoise(x, y, 4, 11);
  81. const swirl = Math.sin((x + n * 40) / 9 + Math.sin(y / 11) * 2);
  82. return [0.9 + n * 0.1 - (swirl > 0.85 ? 0.08 : 0), 0];
  83. },
  84. jelly: (x, y) => {
  85. const deep = valueNoise(x, y, 4, 13);
  86. const { d } = worley(x, y, 8, 14);
  87. const stripe = (x - y + TILE * 4) % 64 < 6 ? 0.25 : 0;
  88. if (d > 0.12 && d < 0.17) return [1.15, 0.35]; // tiny bubble ring
  89. return [0.82 + deep * 0.25, stripe];
  90. },
  91. biscuit: (x, y) => {
  92. const grain = (hash2(x, y, 17) - 0.5) * 0.16;
  93. const row = Math.floor(y / 32);
  94. const hx = (x + (row % 2) * 16) % 32;
  95. const hy = y % 32;
  96. const dx = hx - 16;
  97. const dy = hy - 16;
  98. const r2 = dx * dx + dy * dy;
  99. if (r2 < 9) return [0.6, 0]; // docking hole
  100. if (r2 < 16 && dy > 0) return [1.15, 0.05];
  101. return [1 + grain + (valueNoise(x, y, 8, 18) - 0.5) * 0.1, 0];
  102. },
  103. caramel: (x, y) => {
  104. const streak = valueNoise(x, y * 4, 4, 19);
  105. return [0.85 + streak * 0.3, streak > 0.8 ? 0.15 : 0];
  106. },
  107. wool: (x, y) => {
  108. const u = x % 10;
  109. const v = (y + (u < 5 ? u : 9 - u)) % 10;
  110. return [v < 2 ? 0.75 : 1 + (hash2(x, y, 21) - 0.5) * 0.1, 0];
  111. },
  112. plate: (x, y) => {
  113. const band = y % 14;
  114. if (band < 2) return [1.25, 0.3];
  115. if (band === 13) return [0.85, 0];
  116. return [1.05 + (valueNoise(x, y, 4, 23) - 0.5) * 0.06, 0.1];
  117. },
  118. };
  119. const fallback: Shader = (x, y) => [0.95 + hash2(x, y, 1) * 0.1, 0];
  120. /** Packed 0xRRGGBB tile for a material. */
  121. export function makeTile(color: number, texture: string): Uint32Array {
  122. const shade = shaders[texture] ?? fallback;
  123. const r = color >> 16;
  124. const g = (color >> 8) & 0xff;
  125. const b = color & 0xff;
  126. const tile = new Uint32Array(TILE * TILE);
  127. for (let y = 0; y < TILE; y++) {
  128. for (let x = 0; x < TILE; x++) {
  129. const [k, white] = shade(x, y);
  130. const c = (v: number): number => Math.max(0, Math.min(255, Math.round(v * k * (1 - white) + 255 * white)));
  131. tile[y * TILE + x] = (c(r) << 16) | (c(g) << 8) | c(b);
  132. }
  133. }
  134. return tile;
  135. }
  136. /** Felt-pen outline thickness at a world pixel: 2..max, varying slowly along the line. */
  137. export function makeThicknessTile(max: number): Uint8Array {
  138. const tile = new Uint8Array(TILE * TILE);
  139. for (let y = 0; y < TILE; y++) {
  140. for (let x = 0; x < TILE; x++) tile[y * TILE + x] = 2 + Math.round(valueNoise(x, y, 8, 31) * (max - 2));
  141. }
  142. return tile;
  143. }
  144. export const tileIndex = (x: number, y: number): number => (y & MASK) * TILE + (x & MASK);