- Weltgenerierung: FBM-Terrain, 3 Biome (Plains/Wald/Wüste), Höhlen, Wasser, Bäume - Blöcke: Grass, Dirt, Stein, Sand, Holz, Blätter, Wasser (+Bedrock) - Interaktion: DDA-Voxel-Raycast, Abbauen (LMB) & Setzen (RMB), Hotbar 1-6 - Spieler: First-Person, Gravitation, AABB-Kollision, Springen, Fly-Mode (F) - Engine: Chunk-Streaming (16x16x64), Face-Culling, opaker+transparenter Pass - Stack: Three.js + simplex-noise via CDN, prozedurale Texturen, kein Build-Step Co-Authored-By: Claude <noreply@anthropic.com>
126 lines
4.1 KiB
JavaScript
126 lines
4.1 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)})`;
|
|
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;
|
|
}
|