import * as THREE from 'three'; import { SimplexNoise } from './utils/SimplexNoise.js'; export class TerrainGenerator { constructor(options = {}) { this.size = options.size || 1000; this.segments = options.segments || 256; this.maxHeight = options.maxHeight || 50; this.waterLevel = options.waterLevel || 0; this.beachWidth = options.beachWidth || 20; this.shoreWidth = options.shoreWidth || 3.5; this.shoreDepth = options.shoreDepth || 1.5; this.noise = new SimplexNoise(options.seed || 42); this.terrain = null; } generate() { const geometry = new THREE.PlaneGeometry( this.size, this.size, this.segments, this.segments ); const positions = geometry.attributes.position.array; const colors = new Float32Array(positions.length); for (let i = 0; i < positions.length; i += 3) { const x = positions[i]; const y = positions[i + 1]; let height = this.getHeight(x, y); positions[i + 2] = height; const color = this.getTerrainColor(x, y, height); colors[i] = color.r; colors[i + 1] = color.g; colors[i + 2] = color.b; } geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geometry.computeVertexNormals(); geometry.rotateX(-Math.PI / 2); const material = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.85, metalness: 0.0, flatShading: false }); this.terrain = new THREE.Mesh(geometry, material); this.terrain.receiveShadow = true; this.terrain.castShadow = true; return this.terrain; } getHeight(x, y) { const scale = 0.0008; let height = this.noise.fbm(x * scale, y * scale, 3, 2.0, 0.5); const detail = this.noise.fbm(x * scale * 3, y * scale * 3, 2, 2.0, 0.5) * 0.2; height += detail; const distFromCenter = Math.sqrt(x * x + y * y); const maxLandDist = this.size * 0.45; const falloffStart = this.size * 0.2; let continentMask = 1.0; let edgeDepth = 0; if (distFromCenter > falloffStart) { const t = (distFromCenter - falloffStart) / (maxLandDist - falloffStart); continentMask = Math.max(0, 1 - Math.pow(t, 1.2)); edgeDepth = -15 * Math.pow(Math.max(0, t), 2); } height = height * continentMask + edgeDepth; height = height * this.maxHeight * 0.5; const shoreMin = this.waterLevel - this.shoreWidth; const shoreMax = this.waterLevel + this.shoreWidth; if (height > shoreMin && height < shoreMax) { if (height < this.waterLevel) { const t = (height - shoreMin) / (this.waterLevel - shoreMin); height = this.waterLevel - this.shoreDepth * Math.pow(1 - t, 1.35); } else { const t = (height - this.waterLevel) / (shoreMax - this.waterLevel); height = this.waterLevel + Math.pow(t, 0.75) * this.shoreWidth; } } if (Math.abs(height - this.waterLevel) < 0.06) { height = this.waterLevel - 0.06; } return height; } getTerrainColor(x, y, height) { const normalizedHeight = (height - this.waterLevel) / this.maxHeight; const deepWater = new THREE.Color(0x1a3d5c); const shallowWater = new THREE.Color(0x2d6b8a); const beach = new THREE.Color(0xc2b280); const grass = new THREE.Color(0x3d6b3d); const darkGrass = new THREE.Color(0x2d4a2d); const rock = new THREE.Color(0x5a5a5a); const snow = new THREE.Color(0xe8e8e8); let color = new THREE.Color(); if (height < this.waterLevel - 1.5) { color.copy(deepWater); } else if (height < this.waterLevel) { const shallowBlend = (height - (this.waterLevel - 1.5)) / 1.5; color.lerpColors(deepWater, shallowWater, THREE.MathUtils.clamp(shallowBlend, 0, 1)); } else if (normalizedHeight < 0.08) { color.copy(beach); } else if (normalizedHeight < 0.18) { const sandToGrass = (normalizedHeight - 0.08) / 0.1; color.lerpColors(beach, grass, sandToGrass); } else if (normalizedHeight < 0.4) { const grassBlend = (normalizedHeight - 0.18) / 0.22; color.lerpColors(grass, darkGrass, grassBlend); } else if (normalizedHeight < 0.6) { const rockBlend = (normalizedHeight - 0.4) / 0.2; color.lerpColors(darkGrass, rock, rockBlend); } else if (normalizedHeight < 0.8) { color.copy(rock); } else { const snowBlend = (normalizedHeight - 0.8) / 0.2; color.lerpColors(rock, snow, snowBlend); } const noiseVariation = this.noise.noise2D(x * 0.05, y * 0.05) * 0.1; color.r = Math.max(0, Math.min(1, color.r + noiseVariation)); color.g = Math.max(0, Math.min(1, color.g + noiseVariation)); color.b = Math.max(0, Math.min(1, color.b + noiseVariation)); return color; } getTerrain() { return this.terrain; } getHeightAt(x, z) { return this.getHeight(x, -z); } isLand(x, z) { return this.getHeight(x, z) > this.waterLevel; } }