feat: shadows, baked vertex AO, post-processing + central config.js
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>
This commit is contained in:
1 parent
1049253d54
commit
f009789c42
9 files changed
+282
-59
No files matched your search
+85
-22
@@ -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++;
|
||||
|
||||
Reference in new issue
Block a user