157 lines
5.6 KiB
JavaScript
157 lines
5.6 KiB
JavaScript
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;
|
|
}
|
|
}
|