Files
three-offshore-vibe/src/TerrainGenerator.js
2026-03-25 17:46:03 +08:00

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;
}
}