This commit is contained in:
2026-03-25 17:28:56 +08:00
commit e17b09ff6d
13 changed files with 2466 additions and 0 deletions

305
src/OceanScene.js Normal file
View File

@@ -0,0 +1,305 @@
import * as THREE from 'three';
import { Water } from 'three/addons/objects/Water.js';
import { Sky } from 'three/addons/objects/Sky.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TerrainGenerator } from './TerrainGenerator.js';
import { VegetationSystem } from './VegetationSystem.js';
export class OceanScene {
constructor(container) {
this.container = container;
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.water = null;
this.sky = null;
this.sun = new THREE.Vector3();
this.terrain = null;
this.vegetation = null;
this.pmremGenerator = null;
this.renderTarget = null;
this.sunLight = null;
this.params = {
elevation: 2,
azimuth: 180,
exposure: 0.5,
turbidity: 10,
rayleigh: 2,
mieCoefficient: 0.005,
mieDirectionalG: 0.8
};
this.clock = new THREE.Clock();
this.frameCount = 0;
this.lastTime = performance.now();
}
async init() {
this.initRenderer();
this.initScene();
this.initCamera();
this.initControls();
this.initLighting();
await this.initSky();
await this.initWater();
await this.initTerrain();
await this.initVegetation();
this.initSunPosition();
this.initEventListeners();
}
initRenderer() {
this.renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: 'high-performance'
});
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = this.params.exposure;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.container.appendChild(this.renderer.domElement);
}
initScene() {
this.scene = new THREE.Scene();
this.scene.fog = new THREE.FogExp2(0x8cb8d4, 0.0008);
}
initCamera() {
this.camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
1,
20000
);
this.camera.position.set(100, 50, 200);
}
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.maxPolarAngle = Math.PI * 0.48;
this.controls.minDistance = 30;
this.controls.maxDistance = 1000;
this.controls.target.set(0, 10, 0);
this.controls.update();
}
initLighting() {
const ambientLight = new THREE.AmbientLight(0x555555);
this.scene.add(ambientLight);
this.sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
this.sunLight.castShadow = true;
this.sunLight.shadow.mapSize.width = 2048;
this.sunLight.shadow.mapSize.height = 2048;
this.sunLight.shadow.camera.near = 0.5;
this.sunLight.shadow.camera.far = 500;
this.sunLight.shadow.camera.left = -100;
this.sunLight.shadow.camera.right = 100;
this.sunLight.shadow.camera.top = 100;
this.sunLight.shadow.camera.bottom = -100;
this.scene.add(this.sunLight);
}
async initSky() {
this.sky = new Sky();
this.sky.scale.setScalar(10000);
this.scene.add(this.sky);
const skyUniforms = this.sky.material.uniforms;
skyUniforms['turbidity'].value = this.params.turbidity;
skyUniforms['rayleigh'].value = this.params.rayleigh;
skyUniforms['mieCoefficient'].value = this.params.mieCoefficient;
skyUniforms['mieDirectionalG'].value = this.params.mieDirectionalG;
this.pmremGenerator = new THREE.PMREMGenerator(this.renderer);
}
async initWater() {
const waterGeometry = new THREE.PlaneGeometry(10000, 10000, 128, 128);
const waterNormals = await new Promise((resolve) => {
new THREE.TextureLoader().load(
'https://threejs.org/examples/textures/waternormals.jpg',
(texture) => {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
resolve(texture);
}
);
});
this.water = new Water(waterGeometry, {
textureWidth: 512,
textureHeight: 512,
waterNormals: waterNormals,
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: 0x001e0f,
distortionScale: 3.7,
fog: true
});
this.water.rotation.x = -Math.PI / 2;
this.water.position.y = 0;
this.scene.add(this.water);
}
async initTerrain() {
const terrainGen = new TerrainGenerator({
size: 1200,
segments: 200,
maxHeight: 30,
waterLevel: 0,
seed: 42
});
this.terrain = terrainGen.generate();
this.scene.add(this.terrain);
this.terrainGenerator = terrainGen;
}
async initVegetation() {
const vegSystem = new VegetationSystem(this.terrainGenerator, {
grassCount: 30000,
treeCount: 300,
terrainSize: 1200,
waterLevel: 1
});
this.vegetation = vegSystem.generate();
vegSystem.addToScene(this.scene);
}
initSunPosition() {
this.updateSun();
}
updateSun() {
const phi = THREE.MathUtils.degToRad(90 - this.params.elevation);
const theta = THREE.MathUtils.degToRad(this.params.azimuth);
this.sun.setFromSphericalCoords(1, phi, theta);
this.sky.material.uniforms['sunPosition'].value.copy(this.sun);
this.water.material.uniforms['sunDirection'].value.copy(this.sun).normalize();
if (this.sunLight) {
const sunDistance = 100;
this.sunLight.position.set(
this.sun.x * sunDistance,
this.sun.y * sunDistance,
this.sun.z * sunDistance
);
}
if (this.renderTarget) {
this.renderTarget.dispose();
}
const sceneEnv = new THREE.Scene();
sceneEnv.add(this.sky);
this.renderTarget = this.pmremGenerator.fromScene(sceneEnv);
this.scene.environment = this.renderTarget.texture;
this.scene.add(this.sky);
this.scene.fog.color.setHex(this.getFogColor());
}
getFogColor() {
const elevation = this.params.elevation;
if (elevation < 0) {
return 0x1a2a3a;
} else if (elevation < 10) {
return 0x4a5a6a;
} else if (elevation < 20) {
return 0x8cb8d4;
} else if (elevation < 45) {
return 0x9ec5db;
} else if (elevation < 70) {
return 0xb8d4e8;
} else {
return 0xd4e8f4;
}
}
initEventListeners() {
window.addEventListener('resize', () => this.onWindowResize());
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
setSunElevation(value) {
this.params.elevation = value;
this.updateSun();
}
setSunAzimuth(value) {
this.params.azimuth = value;
this.updateSun();
}
setExposure(value) {
this.params.exposure = value;
this.renderer.toneMappingExposure = value;
}
setTurbidity(value) {
this.params.turbidity = value;
this.sky.material.uniforms['turbidity'].value = value;
this.updateSun();
}
setRayleigh(value) {
this.params.rayleigh = value;
this.sky.material.uniforms['rayleigh'].value = value;
this.updateSun();
}
animate() {
requestAnimationFrame(() => this.animate());
const time = this.clock.getElapsedTime();
if (this.water) {
this.water.material.uniforms['time'].value += 1.0 / 60.0;
}
this.controls.update();
this.renderer.render(this.scene, this.camera);
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.lastTime >= 1000) {
const fps = Math.round(this.frameCount * 1000 / (currentTime - this.lastTime));
const fpsElement = document.getElementById('fps');
if (fpsElement) {
fpsElement.textContent = fps;
}
this.frameCount = 0;
this.lastTime = currentTime;
}
}
hideLoading() {
const loading = document.getElementById('loading');
if (loading) {
loading.style.opacity = '0';
loading.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
loading.style.display = 'none';
}, 500);
}
}
}

142
src/TerrainGenerator.js Normal file
View File

@@ -0,0 +1,142 @@
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.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.25;
let continentMask = 1.0;
if (distFromCenter > falloffStart) {
const t = (distFromCenter - falloffStart) / (maxLandDist - falloffStart);
continentMask = Math.max(0, 1 - Math.pow(t, 1.5));
}
height = height * continentMask;
height = height * this.maxHeight * 0.4;
const beachZone = 2;
if (height > this.waterLevel && height < this.waterLevel + beachZone) {
const blend = Math.max(0, Math.min(1, (height - this.waterLevel) / beachZone));
height = this.waterLevel + Math.pow(blend, 0.5) * beachZone;
}
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 (normalizedHeight < -0.1) {
color.copy(deepWater);
} else if (normalizedHeight < 0) {
color.lerpColors(deepWater, shallowWater, (normalizedHeight + 0.1) / 0.1);
} else if (normalizedHeight < 0.05) {
const beachBlend = Math.min(1, normalizedHeight / 0.05);
color.lerpColors(shallowWater, beach, beachBlend);
} else if (normalizedHeight < 0.15) {
const sandToGrass = (normalizedHeight - 0.05) / 0.1;
color.lerpColors(beach, grass, sandToGrass);
} else if (normalizedHeight < 0.4) {
const grassBlend = (normalizedHeight - 0.15) / 0.25;
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;
}
}

195
src/VegetationSystem.js Normal file
View File

@@ -0,0 +1,195 @@
import * as THREE from 'three';
import { SimplexNoise } from './utils/SimplexNoise.js';
export class VegetationSystem {
constructor(terrain, options = {}) {
this.terrain = terrain;
this.options = {
grassCount: options.grassCount || 50000,
treeCount: options.treeCount || 500,
terrainSize: options.terrainSize || 1000,
waterLevel: options.waterLevel || 0
};
this.noise = new SimplexNoise(12345);
this.grass = null;
this.trees = [];
}
generate() {
this.generateGrass();
this.generateTrees();
return {
grass: this.grass,
trees: this.trees
};
}
generateGrass() {
const geometry = new THREE.BufferGeometry();
const positions = [];
const colors = [];
const uvs = [];
const indices = [];
const grassBladeHeight = 0.8;
const grassBladeWidth = 0.1;
let vertexIndex = 0;
for (let i = 0; i < this.options.grassCount; i++) {
const x = (Math.random() - 0.5) * this.options.terrainSize * 0.7;
const z = (Math.random() - 0.5) * this.options.terrainSize * 0.7;
const y = this.terrain.getHeightAt(x, z);
if (y < this.options.waterLevel + 0.5 || y > this.options.waterLevel + 10) continue;
const bendX = this.noise.noise2D(x * 0.1, z * 0.1) * 0.3;
const bendZ = this.noise.noise2D(x * 0.1 + 100, z * 0.1) * 0.3;
positions.push(
x - grassBladeWidth / 2, y, z,
x + grassBladeWidth / 2, y, z,
x + bendX + grassBladeWidth / 4, y + grassBladeHeight, z + bendZ
);
const greenVariation = 0.7 + Math.random() * 0.3;
const baseColor = new THREE.Color().setHSL(0.3, 0.6 * greenVariation, 0.25 * greenVariation);
colors.push(
baseColor.r, baseColor.g, baseColor.b,
baseColor.r * 0.9, baseColor.g * 0.9, baseColor.b * 0.9,
baseColor.r * 0.8, baseColor.g * 0.8, baseColor.b * 0.8
);
uvs.push(0, 0, 1, 0, 0.5, 1);
indices.push(
vertexIndex, vertexIndex + 1, vertexIndex + 2,
vertexIndex + 2, vertexIndex + 1, vertexIndex
);
vertexIndex += 3;
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geometry.setIndex(indices);
geometry.computeVertexNormals();
const material = new THREE.MeshStandardMaterial({
vertexColors: true,
side: THREE.DoubleSide,
roughness: 0.9,
metalness: 0.0
});
this.grass = new THREE.Mesh(geometry, material);
this.grass.castShadow = true;
this.grass.receiveShadow = true;
}
generateTrees() {
const treePositions = [];
for (let i = 0; i < this.options.treeCount * 10; i++) {
if (treePositions.length >= this.options.treeCount) break;
const x = (Math.random() - 0.5) * this.options.terrainSize * 0.6;
const z = (Math.random() - 0.5) * this.options.terrainSize * 0.6;
const y = this.terrain.getHeightAt(x, z);
if (y < this.options.waterLevel + 1 || y > this.options.waterLevel + 10) continue;
const densityNoise = this.noise.noise2D(x * 0.005, z * 0.005);
if (densityNoise < 0.3) continue;
let tooClose = false;
for (const pos of treePositions) {
const dist = Math.sqrt((pos.x - x) ** 2 + (pos.z - z) ** 2);
if (dist < 10) {
tooClose = true;
break;
}
}
if (!tooClose) {
treePositions.push({ x, y, z });
}
}
for (const pos of treePositions) {
const tree = this.createTree(pos);
this.trees.push(tree);
}
}
createTree(pos) {
const tree = new THREE.Group();
const trunkHeight = 5 + Math.random() * 5;
const trunkRadius = 0.3 + Math.random() * 0.2;
const trunkGeometry = new THREE.CylinderGeometry(
trunkRadius * 0.7,
trunkRadius,
trunkHeight,
8
);
const trunkMaterial = new THREE.MeshStandardMaterial({
color: 0x4a3728,
roughness: 0.9,
metalness: 0.0
});
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
trunk.position.y = trunkHeight / 2;
trunk.castShadow = true;
trunk.receiveShadow = true;
tree.add(trunk);
const foliageLayers = 3 + Math.floor(Math.random() * 2);
let foliageY = trunkHeight;
for (let i = 0; i < foliageLayers; i++) {
const foliageRadius = (2.5 - i * 0.4) + Math.random() * 0.5;
const foliageHeight = 2 + Math.random() * 1;
const foliageGeometry = new THREE.ConeGeometry(
foliageRadius,
foliageHeight,
8
);
const greenVariation = 0.8 + Math.random() * 0.2;
const foliageMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.28 + Math.random() * 0.05, 0.5 * greenVariation, 0.2 * greenVariation),
roughness: 0.8,
metalness: 0.0
});
const foliage = new THREE.Mesh(foliageGeometry, foliageMaterial);
foliage.position.y = foliageY;
foliage.castShadow = true;
foliage.receiveShadow = true;
tree.add(foliage);
foliageY += foliageHeight * 0.6;
}
tree.position.set(pos.x, pos.y, pos.z);
const scale = 0.8 + Math.random() * 0.4;
tree.scale.setScalar(scale);
tree.rotation.y = Math.random() * Math.PI * 2;
return tree;
}
addToScene(scene) {
if (this.grass) scene.add(this.grass);
this.trees.forEach(tree => scene.add(tree));
}
}

76
src/main.js Normal file
View File

@@ -0,0 +1,76 @@
import { OceanScene } from './OceanScene.js';
async function main() {
const container = document.getElementById('container');
const oceanScene = new OceanScene(container);
try {
await oceanScene.init();
setupControls(oceanScene);
oceanScene.hideLoading();
oceanScene.animate();
console.log('写实海洋场景加载完成!');
} catch (error) {
console.error('场景加载失败:', error);
const loading = document.getElementById('loading');
if (loading) {
loading.innerHTML = `
<h1 style="color: #ff6b6b;">加载失败</h1>
<p style="margin-top: 20px; opacity: 0.8;">${error.message}</p>
<p style="margin-top: 10px; opacity: 0.6;">请刷新页面重试</p>
`;
}
}
}
function setupControls(oceanScene) {
const elevationSlider = document.getElementById('sun-elevation');
const azimuthSlider = document.getElementById('sun-azimuth');
const exposureSlider = document.getElementById('exposure');
const turbiditySlider = document.getElementById('turbidity');
const rayleighSlider = document.getElementById('rayleigh');
const elevationValue = document.getElementById('elevation-value');
const azimuthValue = document.getElementById('azimuth-value');
const exposureValue = document.getElementById('exposure-value');
const turbidityValue = document.getElementById('turbidity-value');
const rayleighValue = document.getElementById('rayleigh-value');
elevationSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
oceanScene.setSunElevation(value);
elevationValue.textContent = value.toFixed(1) + '°';
});
azimuthSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
oceanScene.setSunAzimuth(value);
azimuthValue.textContent = value.toFixed(1) + '°';
});
exposureSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
oceanScene.setExposure(value);
exposureValue.textContent = value.toFixed(2);
});
turbiditySlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
oceanScene.setTurbidity(value);
turbidityValue.textContent = value.toFixed(1);
});
rayleighSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
oceanScene.setRayleigh(value);
rayleighValue.textContent = value.toFixed(2);
});
}
main().catch(console.error);

131
src/utils/SimplexNoise.js Normal file
View File

@@ -0,0 +1,131 @@
export class SimplexNoise {
constructor(seed = Math.random()) {
this.p = new Uint8Array(256);
this.perm = new Uint8Array(512);
this.permMod12 = new Uint8Array(512);
const random = this.seededRandom(seed);
for (let i = 0; i < 256; i++) {
this.p[i] = i;
}
for (let i = 255; i > 0; i--) {
const j = Math.floor(random() * (i + 1));
[this.p[i], this.p[j]] = [this.p[j], this.p[i]];
}
for (let i = 0; i < 512; i++) {
this.perm[i] = this.p[i & 255];
this.permMod12[i] = this.perm[i] % 12;
}
this.grad3 = new Float32Array([
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0,
1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1,
0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1
]);
}
seededRandom(seed) {
return function() {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
}
dot2(g, x, y) {
return g[0] * x + g[1] * y;
}
noise2D(xin, yin) {
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
const s = (xin + yin) * F2;
const i = Math.floor(xin + s);
const j = Math.floor(yin + s);
const t = (i + j) * G2;
const X0 = i - t;
const Y0 = j - t;
const x0 = xin - X0;
const y0 = yin - Y0;
let i1, j1;
if (x0 > y0) {
i1 = 1;
j1 = 0;
} else {
i1 = 0;
j1 = 1;
}
const x1 = x0 - i1 + G2;
const y1 = y0 - j1 + G2;
const x2 = x0 - 1 + 2 * G2;
const y2 = y0 - 1 + 2 * G2;
const ii = i & 255;
const jj = j & 255;
let n0 = 0, n1 = 0, n2 = 0;
let t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 >= 0) {
const gi0 = this.permMod12[ii + this.perm[jj]] * 3;
t0 *= t0;
n0 = t0 * t0 * this.dot2([this.grad3[gi0], this.grad3[gi0 + 1]], x0, y0);
}
let t1 = 0.5 - x1 * x1 - y1 * y1;
if (t1 >= 0) {
const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1]] * 3;
t1 *= t1;
n1 = t1 * t1 * this.dot2([this.grad3[gi1], this.grad3[gi1 + 1]], x1, y1);
}
let t2 = 0.5 - x2 * x2 - y2 * y2;
if (t2 >= 0) {
const gi2 = this.permMod12[ii + 1 + this.perm[jj + 1]] * 3;
t2 *= t2;
n2 = t2 * t2 * this.dot2([this.grad3[gi2], this.grad3[gi2 + 1]], x2, y2);
}
return 70 * (n0 + n1 + n2);
}
fbm(x, y, octaves = 6, lacunarity = 2, persistence = 0.5) {
let value = 0;
let amplitude = 1;
let frequency = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * this.noise2D(x * frequency, y * frequency);
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return value / maxValue;
}
ridgedNoise(x, y, octaves = 6, lacunarity = 2, persistence = 0.5) {
let value = 0;
let amplitude = 1;
let frequency = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
let noise = 1 - Math.abs(this.noise2D(x * frequency, y * frequency));
noise = noise * noise;
value += amplitude * noise;
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return value / maxValue;
}
}