Wasser-Tile hatte 2 dunkle Pixel: ein LEAVES-2x2-Block (Seed 333, Offset 15) blutete 1px ueber die Tile-Grenze in die erste Wasser-Spalte. Fix: Dekor- Offsets (LEAVES 2x2, STONE-Adern 2x1) auf TILE_PX-size begrenzen, so dass keine Bloecke mehr ins Nachbar-Tile bluten. Verifiziert: 0 blutende Bloecke. README: Steuerung aktualisiert (Shift = Sprint/Runter statt Ctrl) sowie neue Grafik-Features (Schatten, baked AO, Post-Processing) und config.js ergaenzt. Co-Authored-By: Claude <noreply@anthropic.com>
133 lines
4.5 KiB
JavaScript
133 lines
4.5 KiB
JavaScript
// 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)})`;
|
|
// 2px breite Adern: x auf TILE_PX-2 begrenzen, damit sie nicht ins Nachbar-Tile bluten.
|
|
const sx = Math.min((rStone() * TILE_PX) | 0, TILE_PX - 2);
|
|
const sy = (rStone() * TILE_PX) | 0;
|
|
ctx.fillRect(p.ox + sx, p.oy + sy, 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]})`;
|
|
// 2x2-Bloecke: Offset auf TILE_PX-2 begrenzen, damit sie nicht ins Nachbar-Tile
|
|
// bluten (ein Blatt-Block hatte Offset 15 und erzeugte 2 dunkle Pixel im Wasser).
|
|
const lx = Math.min((rLeaf() * TILE_PX) | 0, TILE_PX - 2);
|
|
const ly = Math.min((rLeaf() * TILE_PX) | 0, TILE_PX - 2);
|
|
ctx.fillRect(p.ox + lx, p.oy + ly, 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;
|
|
}
|