Visuals:
- PCFSoftShadowMap (2048²), directional light follows player with a tight
shadow frustum (SHADOW_AREA) for high texel density; opak blocks cast+receive,
water receives.
- Baked vertex ambient occlusion ("smooth lighting"): per-vertex solid-neighbor
lookup (0fps algorithm) written into a color attribute, with flip-triangulation
to avoid brightness seams across quad diagonals.
- EffectComposer: RenderPass -> UnrealBloomPass (subtle) -> OutputPass with
ACES filmic tone-mapping + sRGB. HalfFloat render target (anti-banding) + MSAA.
Controls:
- Sprint moved from Ctrl to Shift (Ctrl is problematic in browsers). Shift in
fly mode still means "descend" (no conflict, sprint only applies when walking).
config.js:
- New central js/config.js for all tweakable values (world, performance,
renderer/tone-mapping, sky & fog, camera, lighting, shadows, bloom, AO,
player physics, mouse sensitivity, reach) with SHADOWS_ENABLED / BLOOM_ENABLED
feature toggles. Structural engine constants stay in constants.js.
- SEA_LEVEL / WORLD_SEED moved constants.js -> config.js (consumers rewired).
Co-Authored-By: Claude <noreply@anthropic.com>
132 lines
4.8 KiB
JavaScript
132 lines
4.8 KiB
JavaScript
// Weltgenerierung: Terrain, Biome, Hoehlen, Wasser, Baeume.
|
|
// Arbeitet direkt auf Chunk-Block-Arrays. Baeume koennen chunk-uebergreifend
|
|
// in Nachbarn schreiben (world.setGen / world.getGen).
|
|
import { CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, BIOME } from './constants.js';
|
|
import { SEA_LEVEL } from './config.js';
|
|
import {
|
|
noise2DTerrain, noise2DDetail, noise2DTemp, noise2DHumid, noise3DCave, fbm2,
|
|
} from './noise.js';
|
|
import { hash2 } from './rng.js';
|
|
|
|
// Biom aus Temperatur & Feuchtigkeit
|
|
export function getBiome(wx, wz) {
|
|
const temp = noise2DTemp(wx * 0.004, wz * 0.004); // -1..1
|
|
const humid = noise2DHumid(wx * 0.004 + 50, wz * 0.004 - 50);
|
|
if (temp > 0.35 && humid < 0.0) return BIOME.DESERT;
|
|
if (humid > 0.05) return BIOME.FOREST;
|
|
return BIOME.PLAINS;
|
|
}
|
|
|
|
// Terrainhoehe (Ganzzahl) pro Weltsaeule in Abhaengigkeit vom Biom
|
|
export function terrainHeight(wx, wz, biome) {
|
|
// Basis-Huegel
|
|
let n = fbm2(noise2DTerrain, wx * 0.0125, wz * 0.0125, 4, 2.0, 0.5); // -1..1
|
|
let height;
|
|
if (biome === BIOME.DESERT) {
|
|
const dunes = noise2DDetail(wx * 0.03, wz * 0.03) * 0.5;
|
|
height = SEA_LEVEL + 1 + (n * 0.5 + dunes) * 6; // flach
|
|
} else if (biome === BIOME.FOREST) {
|
|
height = SEA_LEVEL + 4 + n * 10;
|
|
} else {
|
|
// Plains mit gelegentlichen Huegeln
|
|
const hill = fbm2(noise2DTerrain, wx * 0.005 + 100, wz * 0.005 - 100, 2, 2.0, 0.5);
|
|
height = SEA_LEVEL + 2 + n * 7 + hill * 6;
|
|
}
|
|
return Math.max(2, Math.min(CHUNK_HEIGHT - 6, Math.round(height)));
|
|
}
|
|
|
|
// Setzt einen Baum (Stamm + Blattkrone) an (wx, groundY, wz)
|
|
function placeTree(world, wx, groundY, wz) {
|
|
const h = 4 + (Math.floor(hash2(wx + 7, wz - 3) * 3)); // 4..6
|
|
for (let i = 1; i <= h; i++) {
|
|
world.setGen(wx, groundY + i, wz, BLOCK.WOOD);
|
|
}
|
|
const topY = groundY + h;
|
|
// Blattkrone
|
|
for (let dy = -2; dy <= 1; dy++) {
|
|
const radius = dy <= -1 ? 2 : 1;
|
|
for (let dx = -radius; dx <= radius; dx++) {
|
|
for (let dz = -radius; dz <= radius; dz++) {
|
|
if (dx === 0 && dz === 0 && dy <= 0) continue; // Stammbereich freilassen
|
|
// Ecken etwas ausduennen
|
|
const dist = Math.abs(dx) + Math.abs(dz) + Math.abs(dy);
|
|
if (dist > 3) continue;
|
|
if ((Math.abs(dx) === radius && Math.abs(dz) === radius) && hash2(wx + dx * 13, wz + dz * 17) < 0.5) continue;
|
|
const ly = topY + dy;
|
|
if (ly < 0 || ly >= CHUNK_HEIGHT) continue;
|
|
if (world.getGen(wx + dx, ly, wz + dz) === BLOCK.AIR) {
|
|
world.setGen(wx + dx, ly, wz + dz, BLOCK.LEAVES);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Spitze
|
|
if (world.getGen(wx, topY + 1, wz) === BLOCK.AIR) {
|
|
world.setGen(wx, topY + 1, wz, BLOCK.LEAVES);
|
|
}
|
|
}
|
|
|
|
// Fuellt Block-Daten eines Chunks (ohne Meshing).
|
|
export function generateChunk(world, chunk) {
|
|
const baseX = chunk.cx * CHUNK_SIZE;
|
|
const baseZ = chunk.cz * CHUNK_SIZE;
|
|
|
|
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
|
|
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
|
|
const wx = baseX + lx;
|
|
const wz = baseZ + lz;
|
|
const biome = getBiome(wx, wz);
|
|
const surface = terrainHeight(wx, wz, biome);
|
|
|
|
for (let y = 0; y <= surface; y++) {
|
|
let block = BLOCK.STONE;
|
|
if (y === 0) {
|
|
block = BLOCK.BEDROCK;
|
|
} else if (y === surface) {
|
|
block = (biome === BIOME.DESERT || surface <= SEA_LEVEL + 1) ? BLOCK.SAND : BLOCK.GRASS;
|
|
} else if (y >= surface - 3) {
|
|
block = (biome === BIOME.DESERT || surface <= SEA_LEVEL + 1) ? BLOCK.SAND : BLOCK.DIRT;
|
|
} else {
|
|
block = BLOCK.STONE;
|
|
}
|
|
|
|
// Hoehlen aushoehlen (nicht ganz unten, nicht an der Oberflaeche)
|
|
if (y > 2 && y < surface - 1) {
|
|
const c = noise3DCave(wx * 0.055, y * 0.09, wz * 0.055);
|
|
if (c > 0.62) block = BLOCK.AIR;
|
|
}
|
|
|
|
if (block !== BLOCK.AIR) chunk.set(lx, y, lz, block);
|
|
}
|
|
|
|
// Wasser auffuellen bis Meereshoehe
|
|
if (surface < SEA_LEVEL) {
|
|
for (let y = surface + 1; y <= SEA_LEVEL; y++) {
|
|
if (chunk.get(lx, y, lz) === BLOCK.AIR) chunk.set(lx, y, lz, BLOCK.WATER);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Baeume (nur in Wald/Plains, oberhalb Wasser, deterministisch)
|
|
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
|
|
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
|
|
const wx = baseX + lx;
|
|
const wz = baseZ + lz;
|
|
const biome = getBiome(wx, wz);
|
|
if (biome === BIOME.DESERT) continue;
|
|
const treeChance = biome === BIOME.FOREST ? 0.028 : 0.006;
|
|
if (hash2(wx * 3 + 1, wz * 5 + 2) < treeChance) {
|
|
// Bodenflaeche finden
|
|
let gy = -1;
|
|
for (let y = CHUNK_HEIGHT - 1; y >= 0; y--) {
|
|
const b = world.getGen(wx, y, wz);
|
|
if (b === BLOCK.GRASS) { gy = y; break; }
|
|
if (b !== BLOCK.AIR && b !== BLOCK.LEAVES && b !== BLOCK.WATER) break;
|
|
}
|
|
if (gy > SEA_LEVEL) placeTree(world, wx, gy, wz);
|
|
}
|
|
}
|
|
}
|
|
}
|