// Prozeduraler Texturatlas: zeichnet alle Block-Tiles auf ein Canvas und // liefert eine THREE.CanvasTexture (Nearest-Filter, Minecraft-Look). import * as THREE from 'three'; import { ATLAS_COLS, TILE_PX, ATLAS_PX, TILE, BLOCK } from './constants.js'; import { mulberry32 } from './rng.js'; // Kleine Helfer: clamp + Farbvariation function clamp(v) { return v < 0 ? 0 : v > 255 ? 255 : v | 0; } function shade(base, delta) { return [clamp(base[0] + delta), clamp(base[1] + delta), clamp(base[2] + delta)]; } // Fuellt ein Tile mit Basisfarbe + Rauschen function paintNoise(ctx, ox, oy, base, vary, seed) { const rand = mulberry32(seed); for (let y = 0; y < TILE_PX; y++) { for (let x = 0; x < TILE_PX; x++) { const d = (rand() - 0.5) * 2 * vary; const c = shade(base, d); ctx.fillStyle = `rgb(${c[0]},${c[1]},${c[2]})`; ctx.fillRect(ox + x, oy + y, 1, 1); } } } export function buildAtlasTexture() { const canvas = document.createElement('canvas'); canvas.width = ATLAS_PX; canvas.height = ATLAS_PX; const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; const t = (index) => ({ ox: (index % ATLAS_COLS) * TILE_PX, oy: Math.floor(index / ATLAS_COLS) * TILE_PX }); // --- DIRT --- let p = t(TILE.DIRT); paintNoise(ctx, p.ox, p.oy, [134, 96, 67], 18, 11); // --- STONE --- p = t(TILE.STONE); paintNoise(ctx, p.ox, p.oy, [128, 128, 132], 16, 23); // ein paar dunklere Adern const rStone = mulberry32(7); for (let i = 0; i < 10; i++) { ctx.fillStyle = `rgb(${clamp(90 + (rStone() - .5) * 20)},${clamp(90 + (rStone() - .5) * 20)},${clamp(94 + (rStone() - .5) * 20)})`; ctx.fillRect(p.ox + ((rStone() * TILE_PX) | 0), p.oy + ((rStone() * TILE_PX) | 0), 2, 1); } // --- SAND --- p = t(TILE.SAND); paintNoise(ctx, p.ox, p.oy, [224, 210, 158], 14, 41); // --- GRASS TOP --- p = t(TILE.GRASS_TOP); paintNoise(ctx, p.ox, p.oy, [102, 168, 64], 22, 5); // --- GRASS SIDE: Dirt mit Gras-Obenrand + Tropfen --- p = t(TILE.GRASS_SIDE); paintNoise(ctx, p.ox, p.oy, [134, 96, 67], 18, 13); // dirt const rGrass = mulberry32(91); // oberer Grasstreifen (ca. oberste 4 px) for (let x = 0; x < TILE_PX; x++) { const h = 3 + ((rGrass() * 3) | 0); // 3..5 px Gras for (let y = 0; y < h; y++) { const d = (rGrass() - 0.5) * 24; const c = shade([102, 168, 64], d); ctx.fillStyle = `rgb(${c[0]},${c[1]},${c[2]})`; ctx.fillRect(p.ox + x, p.oy + y, 1, 1); } } // --- WOOD TOP: Jahresringe --- p = t(TILE.WOOD_TOP); paintNoise(ctx, p.ox, p.oy, [150, 111, 66], 10, 31); ctx.strokeStyle = 'rgb(96,68,38)'; ctx.lineWidth = 1; for (let radius = 2; radius <= 7; radius += 2) { ctx.beginPath(); ctx.arc(p.ox + 7.5, p.oy + 7.5, radius, 0, Math.PI * 2); ctx.stroke(); } // --- WOOD SIDE: Rinde mit vertikalen Streifen --- p = t(TILE.WOOD_SIDE); paintNoise(ctx, p.ox, p.oy, [110, 80, 45], 14, 71); const rBark = mulberry32(123); for (let x = 0; x < TILE_PX; x++) { if (rBark() < 0.4) { ctx.fillStyle = 'rgb(80,56,30)'; for (let y = 0; y < TILE_PX; y++) ctx.fillRect(p.ox + x, p.oy + y, 1, 1); } } // --- LEAVES --- p = t(TILE.LEAVES); paintNoise(ctx, p.ox, p.oy, [62, 130, 44], 30, 55); const rLeaf = mulberry32(333); for (let i = 0; i < 26; i++) { const d = (rLeaf() - 0.5) * 40; const c = shade([44, 100, 30], d); ctx.fillStyle = `rgb(${c[0]},${c[1]},${c[2]})`; ctx.fillRect(p.ox + ((rLeaf() * TILE_PX) | 0), p.oy + ((rLeaf() * TILE_PX) | 0), 2, 2); } // --- WATER (halbtransparent) --- p = t(TILE.WATER); const rWater = mulberry32(999); for (let y = 0; y < TILE_PX; y++) { for (let x = 0; x < TILE_PX; x++) { const d = (rWater() - 0.5) * 18; const c = shade([54, 102, 196], d); ctx.fillStyle = `rgba(${c[0]},${c[1]},${c[2]},0.72)`; ctx.fillRect(p.ox + x, p.oy + y, 1, 1); } } const tex = new THREE.CanvasTexture(canvas); tex.magFilter = THREE.NearestFilter; tex.minFilter = THREE.NearestFilter; tex.generateMipmaps = false; tex.colorSpace = THREE.SRGBColorSpace; tex.wrapS = THREE.ClampToEdgeWrapping; tex.wrapT = THREE.ClampToEdgeWrapping; return tex; }