Files
minecraft-cloneandClaude a748724dfb fix: dark pixels in water texture + update README
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>
2026-06-21 01:11:31 +02:00

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;
}