From f009789c427eb7155d7120f6b02ee6ead6ce67a4 Mon Sep 17 00:00:00 2001 From: minecraft-clone Date: Sun, 21 Jun 2026 00:46:03 +0200 Subject: [PATCH] feat: shadows, baked vertex AO, post-processing + central config.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- index.html | 3 +- js/config.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ js/constants.js | 5 ++- js/main.js | 107 ++++++++++++++++++++++++++++++++++++---------- js/mesher.js | 85 +++++++++++++++++++++++++++--------- js/noise.js | 2 +- js/player.js | 20 ++++----- js/world.js | 3 ++ js/worldgen.js | 5 +-- 9 files changed, 282 insertions(+), 59 deletions(-) create mode 100644 js/config.js diff --git a/index.html b/index.html index 3d1eb56..faaff35 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,7 @@ { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js", + "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.165.0/examples/jsm/", "simplex-noise": "https://cdn.jsdelivr.net/npm/simplex-noise@4.0.1/dist/esm/simplex-noise.js" } } @@ -22,7 +23,7 @@

Klicke, um zu starten.

WASD Laufen   Leertaste Springen
-
Maus Umsehen   Shift Sneak / Runter (Fly)
+
Maus Umsehen   Shift Sprint / Runter (Fly)
Linke Maustaste Abbauen   Rechte Maustaste Setzen
F Flug-Modus   1–6 Block wählen   Esc Pause
diff --git a/js/config.js b/js/config.js new file mode 100644 index 0000000..6967e88 --- /dev/null +++ b/js/config.js @@ -0,0 +1,111 @@ +// ============================================================ +// CONFIG — alle einstellbaren Werte für den Minecraft Clone +// ============================================================ +// Hier kannst du Aussehen, Performance, Welt und Spieler- +// verhalten anpassen. Strukturelle Engine-Konstanten +// (Chunk-Größen, Block-IDs, Textur-Atlas) liegen in +// constants.js und sollten NICHT geändert werden (sonst +// drohen Mesh/Storage-Fehler). +// +// Nach jeder Änderung: Seite HARD-REFRESHEN (Ctrl+Shift+R), +// damit der Browser die Module nicht aus dem Cache lädt. +// ============================================================ + +import * as THREE from 'three'; + +// ──────────────────────────────────────────────────────────── +// 🌍 WELT +// ──────────────────────────────────────────────────────────── +export const WORLD_SEED = 1337; // gleicher Seed = gleiche Welt. Beliebige ganze Zahl. +export const SEA_LEVEL = 26; // Höhe der Wasseroberfläche (muss < CHUNK_HEIGHT=64 bleiben) + +// Spawn-Position (X/Z in Blöcken; Y wird automatisch auf den Boden gesetzt). +export const SPAWN = { x: 8, z: 8 }; + +// ──────────────────────────────────────────────────────────── +// ⚡ PERFORMANCE +// ──────────────────────────────────────────────────────────── +export const RENDER_DISTANCE = 6; // Chunk-Radius um den Spieler (höher = weiter sichtbar, mehr Last) +export const MAX_OPS_PER_FRAME = 2; // Chunk-Generierung/Meshing pro Frame (höher = lädt schneller, ruckelt eher) +export const MAX_PIXEL_RATIO = 2; // Cap der Gerät-Pixelratio (2 = scharf; 1 = schneller auf HiDPI-Screens) + +// ──────────────────────────────────────────────────────────── +// 🎨 RENDERER & TONE-MAPPING +// ──────────────────────────────────────────────────────────── +export const ANTIALIAS = true; // MSAA direkt am Renderer (Kantenglättung ohne Composer) +export const MSAA_SAMPLES = 4; // MSAA-Stufen im Post-Processing-Composer (WebGL2; 0 = aus) +export const TONE_MAPPING = THREE.ACESFilmicToneMapping; // "Color Grading". Alternativen: THREE.Linear / Reinhard / Cineon +export const TONE_MAPPING_EXPOSURE = 1.1; // globale Helligkeit (höher = heller). ACES dunkelt leicht ab. + +// ──────────────────────────────────────────────────────────── +// 🌫️ HIMMEL & NEBEL +// ──────────────────────────────────────────────────────────── +export const SKY_COLOR = 0xaaccff; // Hintergrund- und Nebelfarbe +export const FOG_NEAR = 64; // Block-Distanz, ab der Nebel einsetzt (≈ (RENDER_DISTANCE-2)*16) +export const FOG_FAR = 96; // Block-Distanz, ab der Nebel ganz undurchsichtig ist (≈ RENDER_DISTANCE*16) +// Hinweis: bei Änderung von RENDER_DISTANCE FOG_NEAR/FOG_FAR passend nachziehen. + +// ──────────────────────────────────────────────────────────── +// 📷 KAMERA +// ──────────────────────────────────────────────────────────── +export const CAMERA_FOV = 72; // Sichtfeld in Grad (höher = weiterer Blickwinkel / Fisheye) +export const CAMERA_NEAR = 0.1; +export const CAMERA_FAR = 1000; + +// ──────────────────────────────────────────────────────────── +// 💡 BELEUCHTUNG +// ──────────────────────────────────────────────────────────── +export const HEMISPHERE = { sky: 0xbfdfff, ground: 0x5a6b3a, intensity: 1.05 }; // weiche Aufhellung von oben/unten +export const SUN = { color: 0xfff4d6, intensity: 1.2 }; // gerichtetes Hauptlicht +// Sonnenrichtung (wird intern normalisiert). Bestimmt, aus welcher Seite Licht/Schatten kommen. +export const SUN_DIRECTION = [0.55, 0.72, 0.42]; + +// ──────────────────────────────────────────────────────────── +// 🌑 SCHATTEN +// ──────────────────────────────────────────────────────────── +export const SHADOWS_ENABLED = true; +export const SHADOW_MAP_SIZE = 2048; // Schatten-Textur-Auflösung (höher = schärfer + mehr Last; 1024/2048 typisch) +export const SHADOW_AREA = 44; // halbe Kantenlänge des Schatten-Frustums um den Spieler (Blöcke) +export const SHADOW_BIAS = 0.0002; // gegen "Shadow Acne" (strichartige Selbstschatten) +export const SHADOW_NORMAL_BIAS = 0.02; // gegen Acne auf Voxelflächen (in Normalenrichtung verschieben) + +// ──────────────────────────────────────────────────────────── +// ✨ POST-PROCESSING (Bloom / Glanz) +// ──────────────────────────────────────────────────────────── +export const BLOOM_ENABLED = true; +export const BLOOM = { strength: 0.4, radius: 0.5, threshold: 0.55 }; +// strength = Glanz-Stärke (höher = stärkeres Leuchten) +// radius = Ausbreitung des Glanzes +// threshold = nur Pixel HELLER als dieser Wert glühen (niedriger = mehr glüht) + +// ──────────────────────────────────────────────────────────── +// 🔲 AMBIENT OCCLUSION (Eck-Verdunkelung = "Smooth Lighting") +// ──────────────────────────────────────────────────────────── +// AO-Stufe pro Vertex: Index 0 = stärkste Verdeckung (dunkelste Ecke), +// 3 = keine Verdeckung. Niedrigere Werte = dunklere Ecken/Kanten. +export const AO_LEVEL = [0.2, 0.4, 0.5, 1.8]; + +// ──────────────────────────────────────────────────────────── +// 🚶 SPIELER (Physik & Dimensionen) +// ──────────────────────────────────────────────────────────── +export const PLAYER = { + HALF: 0.3, // halbe Spielerbreite (X/Z) — AABB-Kollisionskasten + HEIGHT: 1.8, // Körperhöhe + EYE: 1.62, // Augenposition über den Füßen + GRAVITY: 26, // Fallbeschleunigung (Blöcke/s²) + JUMP_VEL: 8.6, // Sprung-Startgeschwindigkeit + WALK: 4.7, // Laufgeschwindigkeit (Blöcke/s) + SPRINT: 7.2, // Sprintgeschwindigkeit (Shift) + FLY_SPEED: 9.5, // Fluggeschwindigkeit (F-Modus) + MAX_FALL: 50, // max. Fallgeschwindigkeit (Terminal-Speed) +}; + +// ──────────────────────────────────────────────────────────── +// 🖱️ STEUERUNG +// ──────────────────────────────────────────────────────────── +export const MOUSE_SENSITIVITY = 0.0022; // Maus-Look-Empfindlichkeit (höher = schnellere Drehung) + +// ──────────────────────────────────────────────────────────── +// 🎯 GAMEPLAY +// ──────────────────────────────────────────────────────────── +export const REACH = 6; // Reichweite für Abbauen/Bauen (Blöcke) diff --git a/js/constants.js b/js/constants.js index a19a04b..68559e0 100644 --- a/js/constants.js +++ b/js/constants.js @@ -1,10 +1,11 @@ // Globale Konstanten und Block-/Biome-Definitionen // --- Welt-Dimensionen --- +// Hinweis: SEA_LEVEL und WORLD_SEED sind einstellbar und liegen in config.js. +// Chunk-Größen sind strukturell (Array-/Mesh-Größen) und sollten NICHT geändert +// werden. export const CHUNK_SIZE = 16; // X und Z Ausdehnung eines Chunks export const CHUNK_HEIGHT = 64; // Y Ausdehnung eines Chunks -export const SEA_LEVEL = 26; -export const WORLD_SEED = 1337; // --- Block-Typen --- export const BLOCK = { diff --git a/js/main.js b/js/main.js index 142e9e2..4f3de73 100644 --- a/js/main.js +++ b/js/main.js @@ -1,8 +1,22 @@ // Haupt-Einstiegspunkt: Scene/Camera/Renderer, Licht, Welt, Spieler, Loop. import * as THREE from 'three'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; import { - CHUNK_SIZE, BLOCK, BIOME, HOTBAR, BLOCK_COLOR, + BLOCK, BIOME, HOTBAR, BLOCK_COLOR, } from './constants.js'; +import { + RENDER_DISTANCE, MAX_OPS_PER_FRAME, MAX_PIXEL_RATIO, + ANTIALIAS, MSAA_SAMPLES, TONE_MAPPING, TONE_MAPPING_EXPOSURE, + SKY_COLOR, FOG_NEAR, FOG_FAR, + CAMERA_FOV, CAMERA_NEAR, CAMERA_FAR, + HEMISPHERE, SUN, SUN_DIRECTION, + SHADOWS_ENABLED, SHADOW_MAP_SIZE, SHADOW_AREA, SHADOW_BIAS, SHADOW_NORMAL_BIAS, + BLOOM_ENABLED, BLOOM, + PLAYER, SPAWN, REACH, +} from './config.js'; import { buildAtlasTexture } from './textures.js'; import { World } from './world.js'; import { Player } from './player.js'; @@ -10,42 +24,83 @@ import { Input } from './input.js'; import { raycastVoxel } from './raycast.js'; import { getBiome } from './worldgen.js'; -const RENDER_DISTANCE = 6; // Chunks in jede Richtung -const REACH = 6; // maximale Bau/Abbau-Reichweite -const MAX_OPS_PER_FRAME = 2; // Chunk-Generierung/Meshing pro Frame - const BIOME_NAME = { [BIOME.PLAINS]: 'Plains', [BIOME.FOREST]: 'Wald', [BIOME.DESERT]: 'Wüste' }; // ---------- Renderer / Scene / Camera ---------- const canvas = document.createElement('canvas'); document.body.appendChild(canvas); -const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); -renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); +const renderer = new THREE.WebGLRenderer({ canvas, antialias: ANTIALIAS }); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, MAX_PIXEL_RATIO)); renderer.setSize(window.innerWidth, window.innerHeight); renderer.outputColorSpace = THREE.SRGBColorSpace; +renderer.shadowMap.enabled = SHADOWS_ENABLED; +renderer.shadowMap.type = THREE.PCFSoftShadowMap; +renderer.toneMapping = TONE_MAPPING; +renderer.toneMappingExposure = TONE_MAPPING_EXPOSURE; const scene = new THREE.Scene(); -const SKY = new THREE.Color(0x8fc3ff); -scene.background = SKY; -scene.fog = new THREE.Fog(SKY, (RENDER_DISTANCE - 2) * CHUNK_SIZE, RENDER_DISTANCE * CHUNK_SIZE); +scene.background = new THREE.Color(SKY_COLOR); +scene.fog = new THREE.Fog(SKY_COLOR, FOG_NEAR, FOG_FAR); -const camera = new THREE.PerspectiveCamera(72, window.innerWidth / window.innerHeight, 0.1, 1000); +const camera = new THREE.PerspectiveCamera(CAMERA_FOV, window.innerWidth / window.innerHeight, CAMERA_NEAR, CAMERA_FAR); camera.rotation.order = 'YXZ'; // ---------- Licht ---------- -const hemi = new THREE.HemisphereLight(0xbfdfff, 0x5a6b3a, 0.9); +const hemi = new THREE.HemisphereLight(HEMISPHERE.sky, HEMISPHERE.ground, HEMISPHERE.intensity); scene.add(hemi); -const sun = new THREE.DirectionalLight(0xfff4d6, 0.85); -sun.position.set(60, 120, 30); +const sun = new THREE.DirectionalLight(SUN.color, SUN.intensity); +// Schattenwerfer: orthografische Shadow-Camera, die pro Frame dem Spieler folgt +// (kleines Frustum -> hohe Texeldichte + performant, da ferne Chunks cullt). +sun.castShadow = SHADOWS_ENABLED; +sun.shadow.mapSize.set(SHADOW_MAP_SIZE, SHADOW_MAP_SIZE); +sun.shadow.camera.near = 1; +sun.shadow.camera.far = 260; +// halbe Kantenlaenge (Welt-Einheiten) um den Spieler = SHADOW_AREA aus config +sun.shadow.camera.left = -SHADOW_AREA; +sun.shadow.camera.right = SHADOW_AREA; +sun.shadow.camera.top = SHADOW_AREA; +sun.shadow.camera.bottom = -SHADOW_AREA; +sun.shadow.bias = SHADOW_BIAS; +sun.shadow.normalBias = SHADOW_NORMAL_BIAS; scene.add(sun); +scene.add(sun.target); +// Konstanter Richtungs-Offset; Position/Target werden in der Loop ans Spieler-Auge gesetzt. +const SUN_DIR = new THREE.Vector3(...SUN_DIRECTION).normalize(); +const _sunOffset = SUN_DIR.clone().multiplyScalar(120); // ---------- Texturen & Materialien ---------- +// vertexColors aktiviert: der Mesher liefert pro Vertex einen AO-Wert (Grau- +// stufe), der in den Beleuchtungsterm multipliziert wird -> "Smooth Lighting". const atlas = buildAtlasTexture(); -const matOpaque = new THREE.MeshLambertMaterial({ map: atlas }); +const matOpaque = new THREE.MeshLambertMaterial({ map: atlas, vertexColors: true }); const matWater = new THREE.MeshLambertMaterial({ - map: atlas, transparent: true, depthWrite: false, side: THREE.DoubleSide, + map: atlas, vertexColors: true, transparent: true, depthWrite: false, side: THREE.DoubleSide, }); +// ---------- Post-Processing ---------- +// EffectComposer: RenderPass -> UnrealBloomPass (subtiler Glanz auf hellen +// Flaechen) -> OutputPass (wendet renderer.toneMapping=ACES + sRGB an). +// HalfFloat-Target verhindert Banding im HDR-Bereich; samples=4 aktiviert MSAA, +// da der Composer ansonsten ohne Kantenglaettung in sein Puffer rendert. +const rt = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, { + type: THREE.HalfFloatType, +}); +const composer = new EffectComposer(renderer, rt); +// MSAA auf beiden internen Puffern (WebGL2 erlaubt MSAA auf Float-Targets), +// damit die Szene im Composer kantengeglättet wird. Vorsichtshalber auf beiden +// Targets explizit setzen, falls clone() die Property nicht forterbt. +composer.renderTarget1.samples = MSAA_SAMPLES; +composer.renderTarget2.samples = MSAA_SAMPLES; +composer.addPass(new RenderPass(scene, camera)); +const bloom = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + BLOOM.strength, + BLOOM.radius, + BLOOM.threshold, +); +if (BLOOM_ENABLED) composer.addPass(bloom); +composer.addPass(new OutputPass()); + // ---------- Welt & Spieler ---------- const world = new World(scene, { opaque: matOpaque, water: matWater }); const player = new Player(world); @@ -53,11 +108,11 @@ const input = new Input(canvas); // Spawn-Punkt { - const sx = 8, sz = 8; + const sx = SPAWN.x, sz = SPAWN.z; const groundY = world.highestSolidY(sx, sz); player.setSpawn(sx + 0.5, groundY + 2, sz + 0.5); } -camera.position.set(player.pos[0], player.pos[1] + 1.62, player.pos[2]); +camera.position.set(player.pos[0], player.pos[1] + PLAYER.EYE, player.pos[2]); // Vorab die naechsten Chunks um den Spawn bauen (Boden unter den Fuessen ist // durch highestSolidY ohnehin gesichert; hier geht's nur um den Start-Blick). @@ -111,6 +166,7 @@ window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); }); // ---------- HUD ---------- @@ -128,9 +184,9 @@ function aimDir(out) { // Blockplatzierung pruefen: nicht in den Spieler hineinsetzen function wouldIntersectPlayer(bx, by, bz) { - const minX = player.pos[0] - 0.3, maxX = player.pos[0] + 0.3; - const minY = player.pos[1], maxY = player.pos[1] + 1.8; - const minZ = player.pos[2] - 0.3, maxZ = player.pos[2] + 0.3; + const minX = player.pos[0] - PLAYER.HALF, maxX = player.pos[0] + PLAYER.HALF; + const minY = player.pos[1], maxY = player.pos[1] + PLAYER.HEIGHT; + const minZ = player.pos[2] - PLAYER.HALF, maxZ = player.pos[2] + PLAYER.HALF; return (bx + 1 > minX && bx < maxX && by + 1 > minY && by < maxY && bz + 1 > minZ && bz < maxZ); @@ -186,7 +242,14 @@ function frame(now) { // Streaming (immer, auch im Pause-Menue, damit die Welt laedt) world.updateStreaming(player.pos[0], player.pos[2], RENDER_DISTANCE, MAX_OPS_PER_FRAME); - renderer.render(scene, camera); + // Sonne folgt dem Spieler -> Shadow-Frustum bleibt um den Spieler zentriert. + sun.position.set( + player.pos[0] + _sunOffset.x, player.pos[1] + _sunOffset.y, player.pos[2] + _sunOffset.z + ); + sun.target.position.set(player.pos[0], player.pos[1], player.pos[2]); + sun.target.updateMatrixWorld(); + + composer.render(); // HUD fpsAcc += dt; fpsFrames++; diff --git a/js/mesher.js b/js/mesher.js index cc6a3ad..a8ba49c 100644 --- a/js/mesher.js +++ b/js/mesher.js @@ -3,14 +3,30 @@ // Nachbarn werden erzeugt. import * as THREE from 'three'; import { - CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, ATLAS_COLS, tileFor, isRenderTransparent, + CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, ATLAS_COLS, tileFor, isRenderTransparent, isSolid, } from './constants.js'; +import { AO_LEVEL } from './config.js'; + +// Baked Ambient Occlusion (Minecraft-"Smooth Lighting"): fuer jeden Vertex +// wird aus den soliden Nachbarbloecken in der Aussen-Schicht ein Verdunkelungs- +// wert berechnet. Billiger als SSAO und bei Voxels korrekt (keine Bildraum- +// Artifacts). Siehe 0fps "Ambient occlusion for Minecraft-like worlds". +// Index 0 = staerkste Verdeckung (dunkelste Ecke), 3 = keine Verdeckung. +// Die Stufen-Werte selbst liegen in config.js (AO_LEVEL). + +// side1/side2/corner: je 0 (offen) oder 1 (solid). Liefert AO-Stufe 0..3. +function vertexAO(s1, s2, c) { + if (s1 && s2) return 0; // beide Seiten zu -> Ecke voll verdeckt + return 3 - (s1 + s2 + c); +} // Die 6 Wuerfelseiten mit Eck-Positionen und Tile-UVs (0..1). // side wird aus dir abgeleitet: dir[1]===1 -> 'top', -1 -> 'bottom', sonst 'side'. +// uAxis/vAxis: die zwei zu dir (Normalen-Achse) senkrechten Achsen (0=x,1=y,2=z), +// benoetigt fuer die AO-Nachbar-Lookups je Vertex. const FACES = [ - { // links -X - dir: [-1, 0, 0], + { // links -X (normal-Achse 0) + dir: [-1, 0, 0], uAxis: 2, vAxis: 1, corners: [ { pos: [0, 1, 0], uv: [0, 1] }, { pos: [0, 0, 0], uv: [0, 0] }, @@ -18,8 +34,8 @@ const FACES = [ { pos: [0, 0, 1], uv: [1, 0] }, ], }, - { // rechts +X - dir: [1, 0, 0], + { // rechts +X (normal-Achse 0) + dir: [1, 0, 0], uAxis: 2, vAxis: 1, corners: [ { pos: [1, 1, 1], uv: [0, 1] }, { pos: [1, 0, 1], uv: [0, 0] }, @@ -27,8 +43,8 @@ const FACES = [ { pos: [1, 0, 0], uv: [1, 0] }, ], }, - { // unten -Y - dir: [0, -1, 0], + { // unten -Y (normal-Achse 1) + dir: [0, -1, 0], uAxis: 0, vAxis: 2, corners: [ { pos: [1, 0, 1], uv: [1, 0] }, { pos: [0, 0, 1], uv: [0, 0] }, @@ -36,8 +52,8 @@ const FACES = [ { pos: [0, 0, 0], uv: [0, 1] }, ], }, - { // oben +Y - dir: [0, 1, 0], + { // oben +Y (normal-Achse 1) + dir: [0, 1, 0], uAxis: 0, vAxis: 2, corners: [ { pos: [0, 1, 1], uv: [1, 1] }, { pos: [1, 1, 1], uv: [0, 1] }, @@ -45,8 +61,8 @@ const FACES = [ { pos: [1, 1, 0], uv: [0, 0] }, ], }, - { // hinten -Z - dir: [0, 0, -1], + { // hinten -Z (normal-Achse 2) + dir: [0, 0, -1], uAxis: 0, vAxis: 1, corners: [ { pos: [1, 0, 0], uv: [0, 0] }, { pos: [0, 0, 0], uv: [1, 0] }, @@ -54,8 +70,8 @@ const FACES = [ { pos: [0, 1, 0], uv: [1, 1] }, ], }, - { // vorne +Z - dir: [0, 0, 1], + { // vorne +Z (normal-Achse 2) + dir: [0, 0, 1], uAxis: 0, vAxis: 1, corners: [ { pos: [0, 0, 1], uv: [0, 0] }, { pos: [1, 0, 1], uv: [1, 0] }, @@ -81,21 +97,51 @@ function shouldDrawFace(block, neighbor) { } function makeBuffer() { - return { positions: [], normals: [], uvs: [], indices: [] }; + return { positions: [], normals: [], uvs: [], colors: [], indices: [] }; } -function addFace(buf, x, y, z, face, block) { +function addFace(buf, x, y, z, face, block, getBlockWorld, baseX, baseZ) { const side = sideOf(face.dir); const tile = tileFor(block, side); const ndx = buf.positions.length / 3; - for (const c of face.corners) { + const dir = face.dir; + const ua = face.uAxis, va = face.vAxis; + const wx = baseX + x, wz = baseZ + z; // Welt-Koordinaten des Blocks + const ao = [0, 0, 0, 0]; + + for (let i = 0; i < 4; i++) { + const c = face.corners[i]; buf.positions.push(x + c.pos[0], y + c.pos[1], z + c.pos[2]); - buf.normals.push(face.dir[0], face.dir[1], face.dir[2]); + buf.normals.push(dir[0], dir[1], dir[2]); const u = (tile.col + c.uv[0]) / ATLAS_COLS; const v = 1 - (tile.row + 1 - c.uv[1]) / ATLAS_COLS; buf.uvs.push(u, v); + + // AO: Nachbarn in der Aussen-Schicht (Block + dir). du/dv = -1/+1 je nach + // Ecke. side1 in uAxis-, side2 in vAxis-Richtung, corner diagonal. + const du = c.pos[ua] === 1 ? 1 : -1; + const dv = c.pos[va] === 1 ? 1 : -1; + const s1 = [dir[0], dir[1], dir[2]]; s1[ua] += du; + const s2 = [dir[0], dir[1], dir[2]]; s2[va] += dv; + const co = [dir[0], dir[1], dir[2]]; co[ua] += du; co[va] += dv; + const s1Solid = isSolid(getBlockWorld(wx + s1[0], y + s1[1], wz + s1[2])) ? 1 : 0; + const s2Solid = isSolid(getBlockWorld(wx + s2[0], y + s2[1], wz + s2[2])) ? 1 : 0; + const cSolid = isSolid(getBlockWorld(wx + co[0], y + co[1], wz + co[2])) ? 1 : 0; + + const level = vertexAO(s1Solid, s2Solid, cSolid); + ao[i] = level; + const b = AO_LEVEL[level]; + buf.colors.push(b, b, b); + } + + // Flip-Triangulation: Diagonale entlang der Eck-Paarung mit hoeherer AO-Summe + // legen -> reduziert Interpolations-Artifacts (Helligkeitssprung) an der Naht. + // Winding bleibt erhalten (geprueft: beide Varianten ergeben gleiche Normalen). + if (ao[0] + ao[3] > ao[1] + ao[2]) { + buf.indices.push(ndx, ndx + 1, ndx + 3, ndx, ndx + 3, ndx + 2); + } else { + buf.indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3); } - buf.indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3); } function toGeometry(buf) { @@ -104,6 +150,7 @@ function toGeometry(buf) { g.setAttribute('position', new THREE.Float32BufferAttribute(buf.positions, 3)); g.setAttribute('normal', new THREE.Float32BufferAttribute(buf.normals, 3)); g.setAttribute('uv', new THREE.Float32BufferAttribute(buf.uvs, 2)); + g.setAttribute('color', new THREE.Float32BufferAttribute(buf.colors, 3)); // WICHTIG: setIndex braucht ein BufferAttribute, kein rohes TypedArray. // In three r0.165 speichert setIndex(rohesTypedArray) es ungepackt, so dass // der Renderer beim Render auf attribute.array undefined trifft und crasht @@ -134,7 +181,7 @@ export function buildChunkGeometry(getBlockWorld, chunk) { const nz = z + face.dir[2]; const neighbor = getBlockWorld(baseX + nx, ny, baseZ + nz); if (shouldDrawFace(block, neighbor)) { - addFace(buf, x, y, z, face, block); + addFace(buf, x, y, z, face, block, getBlockWorld, baseX, baseZ); } } } diff --git a/js/noise.js b/js/noise.js index 60a13da..dcdb884 100644 --- a/js/noise.js +++ b/js/noise.js @@ -1,6 +1,6 @@ // Noise-Setup: verschiedene Noise-Funktionen fuer Terrain, Biome und Hoehlen import { createNoise2D, createNoise3D } from 'simplex-noise'; -import { WORLD_SEED } from './constants.js'; +import { WORLD_SEED } from './config.js'; import { mulberry32 } from './rng.js'; // Wir leiten mehrere unabhaengige Streams aus dem Seed ab, damit diff --git a/js/player.js b/js/player.js index 7238388..86e4275 100644 --- a/js/player.js +++ b/js/player.js @@ -1,16 +1,10 @@ // First-Person-Spieler: Bewegung, Gravitation, AABB-Kollision, Springen, // Flug-Modus, Schwimmen. yaw/pitch steuern die Kamera (extern gesetzt). import { isSolid, BLOCK } from './constants.js'; +import { PLAYER, MOUSE_SENSITIVITY } from './config.js'; -const HALF = 0.3; // halbe Spielerbreite (X/Z) -const HEIGHT = 1.8; // Spielerhoehe -const EYE = 1.62; // Augenposition ueber den Fuessen -const GRAVITY = 26; -const JUMP_VEL = 8.6; -const WALK = 4.7; -const SPRINT = 7.2; -const FLY_SPEED = 9.5; -const MAX_FALL = 50; +// Werte aus config.js (PLAYER). Lokal gebunden, damit die Verwendungen kurz bleiben. +const { HALF, HEIGHT, EYE, GRAVITY, JUMP_VEL, WALK, SPRINT, FLY_SPEED, MAX_FALL } = PLAYER; export class Player { constructor(world) { @@ -43,7 +37,7 @@ export class Player { update(input, dt) { // --- Maus-Look --- - const sens = 0.0022; + const sens = MOUSE_SENSITIVITY; this.yaw -= input.mouseDX * sens; this.pitch -= input.mouseDY * sens; const lim = Math.PI / 2 - 0.001; @@ -71,7 +65,11 @@ export class Player { const len = Math.hypot(mx, mz); if (len > 0) { mx /= len; mz /= len; } - const speed = this.flying ? FLY_SPEED : (input.down('ControlLeft') ? SPRINT : WALK); + // Sprint: Shift (Ctrl ist im Browser unbequem – Browser-Shortcuts). + // ShiftLeft/Right dient im Fly-Modus gleichzeitig als "Runter" – kein + // Konflikt, da Sprint nur beim Gehen greift (Fly nutzt konstante FLY_SPEED). + const sprint = input.down('ShiftLeft') || input.down('ShiftRight'); + const speed = this.flying ? FLY_SPEED : (sprint ? SPRINT : WALK); if (this.flying) { this.vel[0] = mx * speed; diff --git a/js/world.js b/js/world.js index b26367d..624a023 100644 --- a/js/world.js +++ b/js/world.js @@ -120,6 +120,8 @@ export class World { const m = new THREE.Mesh(opaque, this.materials.opaque); m.position.set(ox, 0, oz); m.frustumCulled = true; + m.castShadow = true; + m.receiveShadow = true; this.scene.add(m); meshes.opaque = m; } @@ -127,6 +129,7 @@ export class World { const m = new THREE.Mesh(transparent, this.materials.water); m.position.set(ox, 0, oz); m.renderOrder = 1; + m.receiveShadow = true; // Wasseroberflaeche kriegt Schatten; casten unnoetig this.scene.add(m); meshes.water = m; } diff --git a/js/worldgen.js b/js/worldgen.js index bb6d59c..2801974 100644 --- a/js/worldgen.js +++ b/js/worldgen.js @@ -1,9 +1,8 @@ // 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, SEA_LEVEL, BLOCK, BIOME, -} from './constants.js'; +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';