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
+2
-1
@@ -9,6 +9,7 @@
|
|||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js",
|
"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"
|
"simplex-noise": "https://cdn.jsdelivr.net/npm/simplex-noise@4.0.1/dist/esm/simplex-noise.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
<p>Klicke, um zu starten.</p>
|
<p>Klicke, um zu starten.</p>
|
||||||
<div class="keys">
|
<div class="keys">
|
||||||
<div><b>WASD</b> Laufen <b>Leertaste</b> Springen</div>
|
<div><b>WASD</b> Laufen <b>Leertaste</b> Springen</div>
|
||||||
<div><b>Maus</b> Umsehen <b>Shift</b> Sneak / Runter (Fly)</div>
|
<div><b>Maus</b> Umsehen <b>Shift</b> Sprint / Runter (Fly)</div>
|
||||||
<div><b>Linke Maustaste</b> Abbauen <b>Rechte Maustaste</b> Setzen</div>
|
<div><b>Linke Maustaste</b> Abbauen <b>Rechte Maustaste</b> Setzen</div>
|
||||||
<div><b>F</b> Flug-Modus <b>1–6</b> Block wählen <b>Esc</b> Pause</div>
|
<div><b>F</b> Flug-Modus <b>1–6</b> Block wählen <b>Esc</b> Pause</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+111
@@ -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)
|
||||||
+3
-2
@@ -1,10 +1,11 @@
|
|||||||
// Globale Konstanten und Block-/Biome-Definitionen
|
// Globale Konstanten und Block-/Biome-Definitionen
|
||||||
|
|
||||||
// --- Welt-Dimensionen ---
|
// --- 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_SIZE = 16; // X und Z Ausdehnung eines Chunks
|
||||||
export const CHUNK_HEIGHT = 64; // Y Ausdehnung eines Chunks
|
export const CHUNK_HEIGHT = 64; // Y Ausdehnung eines Chunks
|
||||||
export const SEA_LEVEL = 26;
|
|
||||||
export const WORLD_SEED = 1337;
|
|
||||||
|
|
||||||
// --- Block-Typen ---
|
// --- Block-Typen ---
|
||||||
export const BLOCK = {
|
export const BLOCK = {
|
||||||
|
|||||||
+85
-22
@@ -1,8 +1,22 @@
|
|||||||
// Haupt-Einstiegspunkt: Scene/Camera/Renderer, Licht, Welt, Spieler, Loop.
|
// Haupt-Einstiegspunkt: Scene/Camera/Renderer, Licht, Welt, Spieler, Loop.
|
||||||
import * as THREE from 'three';
|
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 {
|
import {
|
||||||
CHUNK_SIZE, BLOCK, BIOME, HOTBAR, BLOCK_COLOR,
|
BLOCK, BIOME, HOTBAR, BLOCK_COLOR,
|
||||||
} from './constants.js';
|
} 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 { buildAtlasTexture } from './textures.js';
|
||||||
import { World } from './world.js';
|
import { World } from './world.js';
|
||||||
import { Player } from './player.js';
|
import { Player } from './player.js';
|
||||||
@@ -10,42 +24,83 @@ import { Input } from './input.js';
|
|||||||
import { raycastVoxel } from './raycast.js';
|
import { raycastVoxel } from './raycast.js';
|
||||||
import { getBiome } from './worldgen.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' };
|
const BIOME_NAME = { [BIOME.PLAINS]: 'Plains', [BIOME.FOREST]: 'Wald', [BIOME.DESERT]: 'Wüste' };
|
||||||
|
|
||||||
// ---------- Renderer / Scene / Camera ----------
|
// ---------- Renderer / Scene / Camera ----------
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
document.body.appendChild(canvas);
|
document.body.appendChild(canvas);
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: ANTIALIAS });
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, MAX_PIXEL_RATIO));
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
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 scene = new THREE.Scene();
|
||||||
const SKY = new THREE.Color(0x8fc3ff);
|
scene.background = new THREE.Color(SKY_COLOR);
|
||||||
scene.background = SKY;
|
scene.fog = new THREE.Fog(SKY_COLOR, FOG_NEAR, FOG_FAR);
|
||||||
scene.fog = new THREE.Fog(SKY, (RENDER_DISTANCE - 2) * CHUNK_SIZE, RENDER_DISTANCE * CHUNK_SIZE);
|
|
||||||
|
|
||||||
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';
|
camera.rotation.order = 'YXZ';
|
||||||
|
|
||||||
// ---------- Licht ----------
|
// ---------- Licht ----------
|
||||||
const hemi = new THREE.HemisphereLight(0xbfdfff, 0x5a6b3a, 0.9);
|
const hemi = new THREE.HemisphereLight(HEMISPHERE.sky, HEMISPHERE.ground, HEMISPHERE.intensity);
|
||||||
scene.add(hemi);
|
scene.add(hemi);
|
||||||
const sun = new THREE.DirectionalLight(0xfff4d6, 0.85);
|
const sun = new THREE.DirectionalLight(SUN.color, SUN.intensity);
|
||||||
sun.position.set(60, 120, 30);
|
// 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);
|
||||||
|
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 ----------
|
// ---------- 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 atlas = buildAtlasTexture();
|
||||||
const matOpaque = new THREE.MeshLambertMaterial({ map: atlas });
|
const matOpaque = new THREE.MeshLambertMaterial({ map: atlas, vertexColors: true });
|
||||||
const matWater = new THREE.MeshLambertMaterial({
|
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 ----------
|
// ---------- Welt & Spieler ----------
|
||||||
const world = new World(scene, { opaque: matOpaque, water: matWater });
|
const world = new World(scene, { opaque: matOpaque, water: matWater });
|
||||||
const player = new Player(world);
|
const player = new Player(world);
|
||||||
@@ -53,11 +108,11 @@ const input = new Input(canvas);
|
|||||||
|
|
||||||
// Spawn-Punkt
|
// Spawn-Punkt
|
||||||
{
|
{
|
||||||
const sx = 8, sz = 8;
|
const sx = SPAWN.x, sz = SPAWN.z;
|
||||||
const groundY = world.highestSolidY(sx, sz);
|
const groundY = world.highestSolidY(sx, sz);
|
||||||
player.setSpawn(sx + 0.5, groundY + 2, sz + 0.5);
|
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
|
// 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).
|
// 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.aspect = window.innerWidth / window.innerHeight;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
composer.setSize(window.innerWidth, window.innerHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- HUD ----------
|
// ---------- HUD ----------
|
||||||
@@ -128,9 +184,9 @@ function aimDir(out) {
|
|||||||
|
|
||||||
// Blockplatzierung pruefen: nicht in den Spieler hineinsetzen
|
// Blockplatzierung pruefen: nicht in den Spieler hineinsetzen
|
||||||
function wouldIntersectPlayer(bx, by, bz) {
|
function wouldIntersectPlayer(bx, by, bz) {
|
||||||
const minX = player.pos[0] - 0.3, maxX = player.pos[0] + 0.3;
|
const minX = player.pos[0] - PLAYER.HALF, maxX = player.pos[0] + PLAYER.HALF;
|
||||||
const minY = player.pos[1], maxY = player.pos[1] + 1.8;
|
const minY = player.pos[1], maxY = player.pos[1] + PLAYER.HEIGHT;
|
||||||
const minZ = player.pos[2] - 0.3, maxZ = player.pos[2] + 0.3;
|
const minZ = player.pos[2] - PLAYER.HALF, maxZ = player.pos[2] + PLAYER.HALF;
|
||||||
return (bx + 1 > minX && bx < maxX &&
|
return (bx + 1 > minX && bx < maxX &&
|
||||||
by + 1 > minY && by < maxY &&
|
by + 1 > minY && by < maxY &&
|
||||||
bz + 1 > minZ && bz < maxZ);
|
bz + 1 > minZ && bz < maxZ);
|
||||||
@@ -186,7 +242,14 @@ function frame(now) {
|
|||||||
// Streaming (immer, auch im Pause-Menue, damit die Welt laedt)
|
// Streaming (immer, auch im Pause-Menue, damit die Welt laedt)
|
||||||
world.updateStreaming(player.pos[0], player.pos[2], RENDER_DISTANCE, MAX_OPS_PER_FRAME);
|
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
|
// HUD
|
||||||
fpsAcc += dt; fpsFrames++;
|
fpsAcc += dt; fpsFrames++;
|
||||||
|
|||||||
+66
-19
@@ -3,14 +3,30 @@
|
|||||||
// Nachbarn werden erzeugt.
|
// Nachbarn werden erzeugt.
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import {
|
import {
|
||||||
CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, ATLAS_COLS, tileFor, isRenderTransparent,
|
CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, ATLAS_COLS, tileFor, isRenderTransparent, isSolid,
|
||||||
} from './constants.js';
|
} 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).
|
// Die 6 Wuerfelseiten mit Eck-Positionen und Tile-UVs (0..1).
|
||||||
// side wird aus dir abgeleitet: dir[1]===1 -> 'top', -1 -> 'bottom', sonst 'side'.
|
// 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 = [
|
const FACES = [
|
||||||
{ // links -X
|
{ // links -X (normal-Achse 0)
|
||||||
dir: [-1, 0, 0],
|
dir: [-1, 0, 0], uAxis: 2, vAxis: 1,
|
||||||
corners: [
|
corners: [
|
||||||
{ pos: [0, 1, 0], uv: [0, 1] },
|
{ pos: [0, 1, 0], uv: [0, 1] },
|
||||||
{ pos: [0, 0, 0], uv: [0, 0] },
|
{ pos: [0, 0, 0], uv: [0, 0] },
|
||||||
@@ -18,8 +34,8 @@ const FACES = [
|
|||||||
{ pos: [0, 0, 1], uv: [1, 0] },
|
{ pos: [0, 0, 1], uv: [1, 0] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ // rechts +X
|
{ // rechts +X (normal-Achse 0)
|
||||||
dir: [1, 0, 0],
|
dir: [1, 0, 0], uAxis: 2, vAxis: 1,
|
||||||
corners: [
|
corners: [
|
||||||
{ pos: [1, 1, 1], uv: [0, 1] },
|
{ pos: [1, 1, 1], uv: [0, 1] },
|
||||||
{ pos: [1, 0, 1], uv: [0, 0] },
|
{ pos: [1, 0, 1], uv: [0, 0] },
|
||||||
@@ -27,8 +43,8 @@ const FACES = [
|
|||||||
{ pos: [1, 0, 0], uv: [1, 0] },
|
{ pos: [1, 0, 0], uv: [1, 0] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ // unten -Y
|
{ // unten -Y (normal-Achse 1)
|
||||||
dir: [0, -1, 0],
|
dir: [0, -1, 0], uAxis: 0, vAxis: 2,
|
||||||
corners: [
|
corners: [
|
||||||
{ pos: [1, 0, 1], uv: [1, 0] },
|
{ pos: [1, 0, 1], uv: [1, 0] },
|
||||||
{ pos: [0, 0, 1], uv: [0, 0] },
|
{ pos: [0, 0, 1], uv: [0, 0] },
|
||||||
@@ -36,8 +52,8 @@ const FACES = [
|
|||||||
{ pos: [0, 0, 0], uv: [0, 1] },
|
{ pos: [0, 0, 0], uv: [0, 1] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ // oben +Y
|
{ // oben +Y (normal-Achse 1)
|
||||||
dir: [0, 1, 0],
|
dir: [0, 1, 0], uAxis: 0, vAxis: 2,
|
||||||
corners: [
|
corners: [
|
||||||
{ pos: [0, 1, 1], uv: [1, 1] },
|
{ pos: [0, 1, 1], uv: [1, 1] },
|
||||||
{ pos: [1, 1, 1], uv: [0, 1] },
|
{ pos: [1, 1, 1], uv: [0, 1] },
|
||||||
@@ -45,8 +61,8 @@ const FACES = [
|
|||||||
{ pos: [1, 1, 0], uv: [0, 0] },
|
{ pos: [1, 1, 0], uv: [0, 0] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ // hinten -Z
|
{ // hinten -Z (normal-Achse 2)
|
||||||
dir: [0, 0, -1],
|
dir: [0, 0, -1], uAxis: 0, vAxis: 1,
|
||||||
corners: [
|
corners: [
|
||||||
{ pos: [1, 0, 0], uv: [0, 0] },
|
{ pos: [1, 0, 0], uv: [0, 0] },
|
||||||
{ pos: [0, 0, 0], uv: [1, 0] },
|
{ pos: [0, 0, 0], uv: [1, 0] },
|
||||||
@@ -54,8 +70,8 @@ const FACES = [
|
|||||||
{ pos: [0, 1, 0], uv: [1, 1] },
|
{ pos: [0, 1, 0], uv: [1, 1] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ // vorne +Z
|
{ // vorne +Z (normal-Achse 2)
|
||||||
dir: [0, 0, 1],
|
dir: [0, 0, 1], uAxis: 0, vAxis: 1,
|
||||||
corners: [
|
corners: [
|
||||||
{ pos: [0, 0, 1], uv: [0, 0] },
|
{ pos: [0, 0, 1], uv: [0, 0] },
|
||||||
{ pos: [1, 0, 1], uv: [1, 0] },
|
{ pos: [1, 0, 1], uv: [1, 0] },
|
||||||
@@ -81,21 +97,51 @@ function shouldDrawFace(block, neighbor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeBuffer() {
|
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 side = sideOf(face.dir);
|
||||||
const tile = tileFor(block, side);
|
const tile = tileFor(block, side);
|
||||||
const ndx = buf.positions.length / 3;
|
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.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 u = (tile.col + c.uv[0]) / ATLAS_COLS;
|
||||||
const v = 1 - (tile.row + 1 - c.uv[1]) / ATLAS_COLS;
|
const v = 1 - (tile.row + 1 - c.uv[1]) / ATLAS_COLS;
|
||||||
buf.uvs.push(u, v);
|
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) {
|
function toGeometry(buf) {
|
||||||
@@ -104,6 +150,7 @@ function toGeometry(buf) {
|
|||||||
g.setAttribute('position', new THREE.Float32BufferAttribute(buf.positions, 3));
|
g.setAttribute('position', new THREE.Float32BufferAttribute(buf.positions, 3));
|
||||||
g.setAttribute('normal', new THREE.Float32BufferAttribute(buf.normals, 3));
|
g.setAttribute('normal', new THREE.Float32BufferAttribute(buf.normals, 3));
|
||||||
g.setAttribute('uv', new THREE.Float32BufferAttribute(buf.uvs, 2));
|
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.
|
// WICHTIG: setIndex braucht ein BufferAttribute, kein rohes TypedArray.
|
||||||
// In three r0.165 speichert setIndex(rohesTypedArray) es ungepackt, so dass
|
// In three r0.165 speichert setIndex(rohesTypedArray) es ungepackt, so dass
|
||||||
// der Renderer beim Render auf attribute.array undefined trifft und crasht
|
// 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 nz = z + face.dir[2];
|
||||||
const neighbor = getBlockWorld(baseX + nx, ny, baseZ + nz);
|
const neighbor = getBlockWorld(baseX + nx, ny, baseZ + nz);
|
||||||
if (shouldDrawFace(block, neighbor)) {
|
if (shouldDrawFace(block, neighbor)) {
|
||||||
addFace(buf, x, y, z, face, block);
|
addFace(buf, x, y, z, face, block, getBlockWorld, baseX, baseZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// Noise-Setup: verschiedene Noise-Funktionen fuer Terrain, Biome und Hoehlen
|
// Noise-Setup: verschiedene Noise-Funktionen fuer Terrain, Biome und Hoehlen
|
||||||
import { createNoise2D, createNoise3D } from 'simplex-noise';
|
import { createNoise2D, createNoise3D } from 'simplex-noise';
|
||||||
import { WORLD_SEED } from './constants.js';
|
import { WORLD_SEED } from './config.js';
|
||||||
import { mulberry32 } from './rng.js';
|
import { mulberry32 } from './rng.js';
|
||||||
|
|
||||||
// Wir leiten mehrere unabhaengige Streams aus dem Seed ab, damit
|
// Wir leiten mehrere unabhaengige Streams aus dem Seed ab, damit
|
||||||
|
|||||||
+9
-11
@@ -1,16 +1,10 @@
|
|||||||
// First-Person-Spieler: Bewegung, Gravitation, AABB-Kollision, Springen,
|
// First-Person-Spieler: Bewegung, Gravitation, AABB-Kollision, Springen,
|
||||||
// Flug-Modus, Schwimmen. yaw/pitch steuern die Kamera (extern gesetzt).
|
// Flug-Modus, Schwimmen. yaw/pitch steuern die Kamera (extern gesetzt).
|
||||||
import { isSolid, BLOCK } from './constants.js';
|
import { isSolid, BLOCK } from './constants.js';
|
||||||
|
import { PLAYER, MOUSE_SENSITIVITY } from './config.js';
|
||||||
|
|
||||||
const HALF = 0.3; // halbe Spielerbreite (X/Z)
|
// Werte aus config.js (PLAYER). Lokal gebunden, damit die Verwendungen kurz bleiben.
|
||||||
const HEIGHT = 1.8; // Spielerhoehe
|
const { HALF, HEIGHT, EYE, GRAVITY, JUMP_VEL, WALK, SPRINT, FLY_SPEED, MAX_FALL } = PLAYER;
|
||||||
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;
|
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
constructor(world) {
|
constructor(world) {
|
||||||
@@ -43,7 +37,7 @@ export class Player {
|
|||||||
|
|
||||||
update(input, dt) {
|
update(input, dt) {
|
||||||
// --- Maus-Look ---
|
// --- Maus-Look ---
|
||||||
const sens = 0.0022;
|
const sens = MOUSE_SENSITIVITY;
|
||||||
this.yaw -= input.mouseDX * sens;
|
this.yaw -= input.mouseDX * sens;
|
||||||
this.pitch -= input.mouseDY * sens;
|
this.pitch -= input.mouseDY * sens;
|
||||||
const lim = Math.PI / 2 - 0.001;
|
const lim = Math.PI / 2 - 0.001;
|
||||||
@@ -71,7 +65,11 @@ export class Player {
|
|||||||
const len = Math.hypot(mx, mz);
|
const len = Math.hypot(mx, mz);
|
||||||
if (len > 0) { mx /= len; mz /= len; }
|
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) {
|
if (this.flying) {
|
||||||
this.vel[0] = mx * speed;
|
this.vel[0] = mx * speed;
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ export class World {
|
|||||||
const m = new THREE.Mesh(opaque, this.materials.opaque);
|
const m = new THREE.Mesh(opaque, this.materials.opaque);
|
||||||
m.position.set(ox, 0, oz);
|
m.position.set(ox, 0, oz);
|
||||||
m.frustumCulled = true;
|
m.frustumCulled = true;
|
||||||
|
m.castShadow = true;
|
||||||
|
m.receiveShadow = true;
|
||||||
this.scene.add(m);
|
this.scene.add(m);
|
||||||
meshes.opaque = m;
|
meshes.opaque = m;
|
||||||
}
|
}
|
||||||
@@ -127,6 +129,7 @@ export class World {
|
|||||||
const m = new THREE.Mesh(transparent, this.materials.water);
|
const m = new THREE.Mesh(transparent, this.materials.water);
|
||||||
m.position.set(ox, 0, oz);
|
m.position.set(ox, 0, oz);
|
||||||
m.renderOrder = 1;
|
m.renderOrder = 1;
|
||||||
|
m.receiveShadow = true; // Wasseroberflaeche kriegt Schatten; casten unnoetig
|
||||||
this.scene.add(m);
|
this.scene.add(m);
|
||||||
meshes.water = m;
|
meshes.water = m;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,9 +1,8 @@
|
|||||||
// Weltgenerierung: Terrain, Biome, Hoehlen, Wasser, Baeume.
|
// Weltgenerierung: Terrain, Biome, Hoehlen, Wasser, Baeume.
|
||||||
// Arbeitet direkt auf Chunk-Block-Arrays. Baeume koennen chunk-uebergreifend
|
// Arbeitet direkt auf Chunk-Block-Arrays. Baeume koennen chunk-uebergreifend
|
||||||
// in Nachbarn schreiben (world.setGen / world.getGen).
|
// in Nachbarn schreiben (world.setGen / world.getGen).
|
||||||
import {
|
import { CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, BIOME } from './constants.js';
|
||||||
CHUNK_SIZE, CHUNK_HEIGHT, SEA_LEVEL, BLOCK, BIOME,
|
import { SEA_LEVEL } from './config.js';
|
||||||
} from './constants.js';
|
|
||||||
import {
|
import {
|
||||||
noise2DTerrain, noise2DDetail, noise2DTemp, noise2DHumid, noise3DCave, fbm2,
|
noise2DTerrain, noise2DDetail, noise2DTemp, noise2DHumid, noise3DCave, fbm2,
|
||||||
} from './noise.js';
|
} from './noise.js';
|
||||||
|
|||||||
Reference in new issue
Block a user