Spaces:
Sleeping
Sleeping
feat: enhance terrain generation with varied biomes, improved mountain shapes, and dynamic river systems
Browse files- README.md +1 -1
- src/Aircraft.js +27 -23
- src/main.js +379 -105
README.md
CHANGED
|
@@ -58,7 +58,7 @@ This project is pre-configured to be deployed as a **Docker Space** on Hugging F
|
|
| 58 |
- **Dynamic Day/Night Cycle**: A slow 7-minute continuous loop transitioning the sky gradient through dawn, noon, vivid sunset, and deep starry night.
|
| 59 |
- **Lighting & Shadows**: PCF Soft Shadows enabled. Dynamic shifting hemisphere and directional orbital lighting.
|
| 60 |
- **Volumetric Fog**: Deep atmospheric fog that shifts colors based on the time of day.
|
| 61 |
-
- **Low-Poly Art Style**:
|
| 62 |
- **Reflective Water**: Beautifully refractive lake surfaces mirroring the sun and sky colors.
|
| 63 |
|
| 64 |
### HUD (Heads-Up Display)
|
|
|
|
| 58 |
- **Dynamic Day/Night Cycle**: A slow 7-minute continuous loop transitioning the sky gradient through dawn, noon, vivid sunset, and deep starry night.
|
| 59 |
- **Lighting & Shadows**: PCF Soft Shadows enabled. Dynamic shifting hemisphere and directional orbital lighting.
|
| 60 |
- **Volumetric Fog**: Deep atmospheric fog that shifts colors based on the time of day.
|
| 61 |
+
- **Low-Poly Art Style**: Stylized terrain with jagged, procedurally generated fractal mountains, snow caps, and forests for a sharp, premium aesthetic.
|
| 62 |
- **Reflective Water**: Beautifully refractive lake surfaces mirroring the sun and sky colors.
|
| 63 |
|
| 64 |
### HUD (Heads-Up Display)
|
src/Aircraft.js
CHANGED
|
@@ -260,16 +260,14 @@ export class Aircraft {
|
|
| 260 |
}
|
| 261 |
|
| 262 |
handleMouseMove(event) {
|
| 263 |
-
// Only apply mouse controls if pointer is locked
|
| 264 |
if (document.pointerLockElement === document.body) {
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
//
|
| 269 |
-
this.
|
| 270 |
-
|
| 271 |
-
// Map mouse X to Roll
|
| 272 |
-
this.roll = THREE.MathUtils.clamp(this.roll + event.movementX * mouseSensX, -1, 1);
|
| 273 |
}
|
| 274 |
}
|
| 275 |
|
|
@@ -319,10 +317,22 @@ export class Aircraft {
|
|
| 319 |
this.cruiseThrust = THREE.MathUtils.clamp(this.cruiseThrust - 0.4 * deltaTime, 0, 1);
|
| 320 |
}
|
| 321 |
|
|
|
|
|
|
|
|
|
|
| 322 |
const speed = Math.max(this.velocity.length(), 0.01);
|
| 323 |
const speedRatio = THREE.MathUtils.clamp(speed / 80, 0.35, 1.6);
|
| 324 |
|
| 325 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
this.rotation.x += this.pitch * this.pitchRate * deltaTime;
|
| 327 |
this.rotation.z += this.roll * this.rollRate * deltaTime;
|
| 328 |
this.rotation.y += this.yaw * this.yawRate * deltaTime;
|
|
@@ -330,22 +340,16 @@ export class Aircraft {
|
|
| 330 |
// Banked turns: roll naturally induces yaw at speed
|
| 331 |
this.rotation.y -= this.rotation.z * this.turnAssist * speedRatio * deltaTime;
|
| 332 |
|
| 333 |
-
//
|
| 334 |
-
|
| 335 |
-
this.pitch = THREE.MathUtils.lerp(this.pitch, 0, deltaTime * 2.0);
|
| 336 |
-
this.roll = THREE.MathUtils.lerp(this.roll, 0, deltaTime * 2.0);
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
// Clamp pitch/roll rotation limits and gently stabilize when no input
|
| 340 |
-
this.rotation.x = THREE.MathUtils.clamp(this.rotation.x, -1.1, 1.1);
|
| 341 |
this.rotation.z = THREE.MathUtils.clamp(this.rotation.z, -1.2, 1.2);
|
| 342 |
|
| 343 |
-
// Auto-leveling
|
| 344 |
-
if (Math.abs(this.
|
| 345 |
-
this.rotation.
|
| 346 |
}
|
| 347 |
-
if (Math.abs(this.
|
| 348 |
-
this.rotation.
|
| 349 |
}
|
| 350 |
|
| 351 |
// Calculate body axes
|
|
|
|
| 260 |
}
|
| 261 |
|
| 262 |
handleMouseMove(event) {
|
| 263 |
+
// Only apply mouse controls if pointer is locked
|
| 264 |
if (document.pointerLockElement === document.body) {
|
| 265 |
+
// Accumulate mouse delta into target inputs (very low sensitivity)
|
| 266 |
+
this._pitchInput += -event.movementY * 0.002;
|
| 267 |
+
this._rollInput += event.movementX * 0.002;
|
| 268 |
+
// Clamp so you can't overload the buffer
|
| 269 |
+
this._pitchInput = THREE.MathUtils.clamp(this._pitchInput, -1, 1);
|
| 270 |
+
this._rollInput = THREE.MathUtils.clamp(this._rollInput, -1, 1);
|
|
|
|
|
|
|
| 271 |
}
|
| 272 |
}
|
| 273 |
|
|
|
|
| 317 |
this.cruiseThrust = THREE.MathUtils.clamp(this.cruiseThrust - 0.4 * deltaTime, 0, 1);
|
| 318 |
}
|
| 319 |
|
| 320 |
+
// Initialize smoothed input buffers
|
| 321 |
+
if (this._pitchInput === undefined) { this._pitchInput = 0; this._rollInput = 0; }
|
| 322 |
+
|
| 323 |
const speed = Math.max(this.velocity.length(), 0.01);
|
| 324 |
const speedRatio = THREE.MathUtils.clamp(speed / 80, 0.35, 1.6);
|
| 325 |
|
| 326 |
+
// ── Smooth input: lerp actual control values toward buffered inputs ──
|
| 327 |
+
this.pitch = THREE.MathUtils.lerp(this.pitch, this._pitchInput, deltaTime * 6);
|
| 328 |
+
this.roll = THREE.MathUtils.lerp(this.roll, this._rollInput, deltaTime * 6);
|
| 329 |
+
|
| 330 |
+
// Decay the input buffer back toward zero (spring-return joystick feel)
|
| 331 |
+
this._pitchInput = THREE.MathUtils.lerp(this._pitchInput, 0, deltaTime * 3);
|
| 332 |
+
this._rollInput = THREE.MathUtils.lerp(this._rollInput, 0, deltaTime * 3);
|
| 333 |
+
|
| 334 |
+
// ── Apply rotation rates using YXZ Euler order ──
|
| 335 |
+
this.rotation.order = 'YXZ';
|
| 336 |
this.rotation.x += this.pitch * this.pitchRate * deltaTime;
|
| 337 |
this.rotation.z += this.roll * this.rollRate * deltaTime;
|
| 338 |
this.rotation.y += this.yaw * this.yawRate * deltaTime;
|
|
|
|
| 340 |
// Banked turns: roll naturally induces yaw at speed
|
| 341 |
this.rotation.y -= this.rotation.z * this.turnAssist * speedRatio * deltaTime;
|
| 342 |
|
| 343 |
+
// Clamp pitch to ±60° (well away from ±90° gimbal lock zone)
|
| 344 |
+
this.rotation.x = THREE.MathUtils.clamp(this.rotation.x, -1.05, 1.05);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
this.rotation.z = THREE.MathUtils.clamp(this.rotation.z, -1.2, 1.2);
|
| 346 |
|
| 347 |
+
// Auto-leveling: gently stabilise when input is near zero
|
| 348 |
+
if (Math.abs(this.pitch) < 0.03 && Math.abs(this._pitchInput) < 0.03) {
|
| 349 |
+
this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, 0, deltaTime * 0.4);
|
| 350 |
}
|
| 351 |
+
if (Math.abs(this.roll) < 0.03 && Math.abs(this._rollInput) < 0.03) {
|
| 352 |
+
this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, 0, deltaTime * 1.0);
|
| 353 |
}
|
| 354 |
|
| 355 |
// Calculate body axes
|
src/main.js
CHANGED
|
@@ -388,17 +388,23 @@ class FlightSimulator {
|
|
| 388 |
}
|
| 389 |
|
| 390 |
createTerrain() {
|
| 391 |
-
const size =
|
| 392 |
-
const segments =
|
| 393 |
const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
|
| 394 |
|
| 395 |
const position = geometry.attributes.position;
|
| 396 |
const colors = [];
|
| 397 |
-
|
| 398 |
-
const
|
| 399 |
-
const
|
| 400 |
-
const
|
| 401 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
const color = new THREE.Color();
|
| 403 |
const blend = new THREE.Color();
|
| 404 |
|
|
@@ -412,31 +418,48 @@ class FlightSimulator {
|
|
| 412 |
height = THREE.MathUtils.lerp(height, 1.4, runwayProximity * 0.76);
|
| 413 |
position.setZ(i, height);
|
| 414 |
|
| 415 |
-
|
|
|
|
| 416 |
const slopeX = this.getTerrainHeight(x + sampleStep, y) - this.getTerrainHeight(x - sampleStep, y);
|
| 417 |
const slopeY = this.getTerrainHeight(x, y + sampleStep) - this.getTerrainHeight(x, y - sampleStep);
|
| 418 |
const slope = Math.abs(slopeX) + Math.abs(slopeY);
|
| 419 |
-
const slopeFactor = THREE.MathUtils.clamp(slope /
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
if (height < -
|
| 427 |
-
color.copy(
|
| 428 |
-
} else if (height <
|
| 429 |
-
blend.lerpColors(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
color.copy(blend);
|
| 431 |
-
} else if (height <
|
| 432 |
-
blend.lerpColors(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
color.copy(blend);
|
| 434 |
} else {
|
| 435 |
-
blend.lerpColors(
|
| 436 |
color.copy(blend);
|
| 437 |
}
|
| 438 |
|
| 439 |
-
color
|
|
|
|
| 440 |
color.offsetHSL(0, 0, variation);
|
| 441 |
colors.push(color.r, color.g, color.b);
|
| 442 |
}
|
|
@@ -509,16 +532,35 @@ class FlightSimulator {
|
|
| 509 |
}
|
| 510 |
|
| 511 |
getTerrainHeight(x, y) {
|
|
|
|
| 512 |
const continental =
|
| 513 |
-
Math.sin(x * 0.
|
| 514 |
-
Math.cos(y * 0.
|
|
|
|
|
|
|
|
|
|
| 515 |
const ridges =
|
| 516 |
-
Math.sin((x + y) * 0.
|
| 517 |
-
Math.cos((x - y) * 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
const details =
|
| 519 |
-
Math.sin(x * 0.
|
| 520 |
-
Math.sin((x + y) * 0.
|
| 521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
}
|
| 523 |
|
| 524 |
createSky() {
|
|
@@ -716,152 +758,329 @@ class FlightSimulator {
|
|
| 716 |
|
| 717 |
createDistantMountains() {
|
| 718 |
const mountainGroup = new THREE.Group();
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
metalness: 0.05,
|
| 723 |
flatShading: true
|
| 724 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
const foothillMaterial = new THREE.MeshStandardMaterial({
|
| 726 |
-
color:
|
| 727 |
-
roughness: 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
metalness: 0.01,
|
| 729 |
flatShading: true
|
| 730 |
});
|
| 731 |
|
| 732 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
const angle = this.randomRange(0, Math.PI * 2);
|
| 734 |
-
const radius = this.randomRange(
|
| 735 |
const x = Math.cos(angle) * radius;
|
| 736 |
const z = Math.sin(angle) * radius;
|
| 737 |
const ground = this.getTerrainHeight(x, z);
|
| 738 |
-
const peakHeight = this.randomRange(95, 250);
|
| 739 |
-
const baseRadius = this.randomRange(34, 92);
|
| 740 |
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
mountain.rotation.y = this.randomRange(0, Math.PI * 2);
|
| 747 |
mountain.castShadow = true;
|
| 748 |
mountain.receiveShadow = true;
|
| 749 |
mountainGroup.add(mountain);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
}
|
| 751 |
|
| 752 |
-
|
|
|
|
| 753 |
const angle = this.randomRange(0, Math.PI * 2);
|
| 754 |
-
const radius = this.randomRange(
|
| 755 |
const x = Math.cos(angle) * radius;
|
| 756 |
const z = Math.sin(angle) * radius;
|
| 757 |
const ground = this.getTerrainHeight(x, z);
|
| 758 |
-
const hillHeight = this.randomRange(24, 72);
|
| 759 |
-
const hillRadius = this.randomRange(20, 55);
|
| 760 |
|
| 761 |
-
const
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
hill.rotation.y = this.randomRange(0, Math.PI * 2);
|
| 767 |
hill.castShadow = true;
|
| 768 |
hill.receiveShadow = true;
|
| 769 |
mountainGroup.add(hill);
|
| 770 |
}
|
| 771 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
this.scene.add(mountainGroup);
|
| 773 |
}
|
| 774 |
|
| 775 |
createForest() {
|
| 776 |
-
const maxTrees =
|
| 777 |
-
const trunkGeometry = new THREE.CylinderGeometry(0.
|
| 778 |
-
const canopyGeometry = new THREE.ConeGeometry(1.
|
|
|
|
|
|
|
| 779 |
const trunkMaterial = new THREE.MeshStandardMaterial({
|
| 780 |
color: 0x3d2719,
|
| 781 |
roughness: 0.98,
|
| 782 |
metalness: 0,
|
| 783 |
flatShading: true
|
| 784 |
});
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
roughness: 0.93,
|
| 788 |
-
metalness: 0.01,
|
| 789 |
-
flatShading: true
|
| 790 |
-
|
|
|
|
| 791 |
|
| 792 |
const trunks = new THREE.InstancedMesh(trunkGeometry, trunkMaterial, maxTrees);
|
| 793 |
-
const canopies = new THREE.InstancedMesh(canopyGeometry,
|
|
|
|
| 794 |
trunks.castShadow = true;
|
| 795 |
trunks.receiveShadow = true;
|
| 796 |
canopies.castShadow = true;
|
| 797 |
canopies.receiveShadow = true;
|
|
|
|
|
|
|
| 798 |
|
| 799 |
const dummy = new THREE.Object3D();
|
| 800 |
|
| 801 |
-
let
|
|
|
|
| 802 |
let attempts = 0;
|
| 803 |
-
while (
|
| 804 |
attempts += 1;
|
| 805 |
-
const x = this.randomRange(-
|
| 806 |
-
const z = this.randomRange(-
|
| 807 |
-
if (Math.sqrt(x * x + z * z) <
|
| 808 |
|
| 809 |
const height = this.getTerrainHeight(x, z);
|
| 810 |
-
if (height < 2 || height >
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
const
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
}
|
| 827 |
|
| 828 |
-
trunks.count =
|
| 829 |
-
canopies.count =
|
|
|
|
| 830 |
trunks.instanceMatrix.needsUpdate = true;
|
| 831 |
canopies.instanceMatrix.needsUpdate = true;
|
|
|
|
| 832 |
this.scene.add(trunks);
|
| 833 |
this.scene.add(canopies);
|
|
|
|
| 834 |
}
|
| 835 |
|
| 836 |
createLake() {
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
const
|
| 841 |
-
|
|
|
|
| 842 |
const h = this.getTerrainHeight(x, z);
|
| 843 |
if (h < lowestSpot.h) {
|
| 844 |
lowestSpot = { x, z, h };
|
| 845 |
}
|
| 846 |
}
|
| 847 |
|
| 848 |
-
const lakeRadius =
|
| 849 |
-
const waterLevel = lowestSpot.h +
|
| 850 |
|
| 851 |
-
//
|
| 852 |
const lakeMaterial = new THREE.MeshStandardMaterial({
|
| 853 |
-
color:
|
| 854 |
-
emissive:
|
| 855 |
-
emissiveIntensity: 0.
|
| 856 |
-
roughness: 0.
|
| 857 |
-
metalness: 0.
|
| 858 |
transparent: true,
|
| 859 |
-
opacity: 0.
|
| 860 |
flatShading: true
|
| 861 |
});
|
| 862 |
|
| 863 |
const lake = new THREE.Mesh(
|
| 864 |
-
new THREE.CircleGeometry(lakeRadius,
|
| 865 |
lakeMaterial
|
| 866 |
);
|
| 867 |
lake.rotation.x = -Math.PI / 2;
|
|
@@ -869,19 +1088,74 @@ class FlightSimulator {
|
|
| 869 |
lake.receiveShadow = true;
|
| 870 |
this.scene.add(lake);
|
| 871 |
|
|
|
|
| 872 |
const shore = new THREE.Mesh(
|
| 873 |
-
new THREE.RingGeometry(lakeRadius, lakeRadius +
|
| 874 |
new THREE.MeshStandardMaterial({
|
| 875 |
-
color:
|
| 876 |
-
roughness: 0.
|
| 877 |
metalness: 0,
|
| 878 |
flatShading: true
|
| 879 |
})
|
| 880 |
);
|
| 881 |
shore.rotation.x = -Math.PI / 2;
|
| 882 |
-
shore.position.set(lowestSpot.x, waterLevel - 0.
|
| 883 |
shore.receiveShadow = true;
|
| 884 |
this.scene.add(shore);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 885 |
}
|
| 886 |
|
| 887 |
randomRange(min, max) {
|
|
|
|
| 388 |
}
|
| 389 |
|
| 390 |
createTerrain() {
|
| 391 |
+
const size = 3200;
|
| 392 |
+
const segments = 300;
|
| 393 |
const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
|
| 394 |
|
| 395 |
const position = geometry.attributes.position;
|
| 396 |
const colors = [];
|
| 397 |
+
// Richer, more varied biome colors
|
| 398 |
+
const deepWaterColor = new THREE.Color(0x0a1a24);
|
| 399 |
+
const shallowWaterColor = new THREE.Color(0x1a3a3a);
|
| 400 |
+
const sandColor = new THREE.Color(0x8a7a5a);
|
| 401 |
+
const lowlandColor = new THREE.Color(0x1a3520);
|
| 402 |
+
const grassColor = new THREE.Color(0x2d5a2d);
|
| 403 |
+
const forestColor = new THREE.Color(0x1f4228);
|
| 404 |
+
const highlandColor = new THREE.Color(0x4a5a40);
|
| 405 |
+
const rockyColor = new THREE.Color(0x5a5850);
|
| 406 |
+
const alpineColor = new THREE.Color(0x7a7870);
|
| 407 |
+
const snowColor = new THREE.Color(0xd8dee8);
|
| 408 |
const color = new THREE.Color();
|
| 409 |
const blend = new THREE.Color();
|
| 410 |
|
|
|
|
| 418 |
height = THREE.MathUtils.lerp(height, 1.4, runwayProximity * 0.76);
|
| 419 |
position.setZ(i, height);
|
| 420 |
|
| 421 |
+
// Calculate slope for rock faces
|
| 422 |
+
const sampleStep = 6;
|
| 423 |
const slopeX = this.getTerrainHeight(x + sampleStep, y) - this.getTerrainHeight(x - sampleStep, y);
|
| 424 |
const slopeY = this.getTerrainHeight(x, y + sampleStep) - this.getTerrainHeight(x, y - sampleStep);
|
| 425 |
const slope = Math.abs(slopeX) + Math.abs(slopeY);
|
| 426 |
+
const slopeFactor = THREE.MathUtils.clamp(slope / 35, 0, 1);
|
| 427 |
+
|
| 428 |
+
// Add subtle color variation
|
| 429 |
+
const noiseVar = Math.sin(x * 0.018 + y * 0.012) * 0.5 + Math.cos(y * 0.024 + x * 0.016) * 0.5;
|
| 430 |
+
const variation = noiseVar * 0.025;
|
| 431 |
+
|
| 432 |
+
// Height-based biomes with smooth transitions
|
| 433 |
+
if (height < -8) {
|
| 434 |
+
color.copy(deepWaterColor);
|
| 435 |
+
} else if (height < -2) {
|
| 436 |
+
blend.lerpColors(deepWaterColor, shallowWaterColor, THREE.MathUtils.clamp((height + 8) / 6, 0, 1));
|
| 437 |
+
color.copy(blend);
|
| 438 |
+
} else if (height < 2) {
|
| 439 |
+
blend.lerpColors(shallowWaterColor, sandColor, THREE.MathUtils.clamp((height + 2) / 4, 0, 1));
|
| 440 |
+
color.copy(blend);
|
| 441 |
+
} else if (height < 8) {
|
| 442 |
+
blend.lerpColors(sandColor, grassColor, THREE.MathUtils.clamp((height - 2) / 6, 0, 1));
|
| 443 |
+
color.copy(blend);
|
| 444 |
+
} else if (height < 20) {
|
| 445 |
+
blend.lerpColors(grassColor, forestColor, THREE.MathUtils.clamp((height - 8) / 12, 0, 1));
|
| 446 |
color.copy(blend);
|
| 447 |
+
} else if (height < 40) {
|
| 448 |
+
blend.lerpColors(forestColor, highlandColor, THREE.MathUtils.clamp((height - 20) / 20, 0, 1));
|
| 449 |
+
color.copy(blend);
|
| 450 |
+
} else if (height < 70) {
|
| 451 |
+
blend.lerpColors(highlandColor, rockyColor, THREE.MathUtils.clamp((height - 40) / 30, 0, 1));
|
| 452 |
+
color.copy(blend);
|
| 453 |
+
} else if (height < 100) {
|
| 454 |
+
blend.lerpColors(rockyColor, alpineColor, THREE.MathUtils.clamp((height - 70) / 30, 0, 1));
|
| 455 |
color.copy(blend);
|
| 456 |
} else {
|
| 457 |
+
blend.lerpColors(alpineColor, snowColor, THREE.MathUtils.clamp((height - 100) / 40, 0, 1));
|
| 458 |
color.copy(blend);
|
| 459 |
}
|
| 460 |
|
| 461 |
+
// Blend in rock color on steep slopes
|
| 462 |
+
color.lerp(rockyColor, slopeFactor * 0.6);
|
| 463 |
color.offsetHSL(0, 0, variation);
|
| 464 |
colors.push(color.r, color.g, color.b);
|
| 465 |
}
|
|
|
|
| 532 |
}
|
| 533 |
|
| 534 |
getTerrainHeight(x, y) {
|
| 535 |
+
// Large continental features - mountain ranges
|
| 536 |
const continental =
|
| 537 |
+
Math.sin(x * 0.0008) * 35 +
|
| 538 |
+
Math.cos(y * 0.00065) * 28 +
|
| 539 |
+
Math.sin((x * 0.0005 + y * 0.0004)) * 22;
|
| 540 |
+
|
| 541 |
+
// Medium ridgelines - create spine-like formations
|
| 542 |
const ridges =
|
| 543 |
+
Math.sin((x + y) * 0.0022) * 18 +
|
| 544 |
+
Math.cos((x - y) * 0.0018) * 14 +
|
| 545 |
+
Math.sin(x * 0.0035) * Math.cos(y * 0.0028) * 12;
|
| 546 |
+
|
| 547 |
+
// Smaller hills and valleys
|
| 548 |
+
const hills =
|
| 549 |
+
Math.sin(x * 0.008) * Math.cos(y * 0.006) * 8 +
|
| 550 |
+
Math.cos(y * 0.009) * Math.sin(x * 0.007) * 6;
|
| 551 |
+
|
| 552 |
+
// Fine detail and texture
|
| 553 |
const details =
|
| 554 |
+
Math.sin(x * 0.025) * Math.cos(y * 0.022) * 3 +
|
| 555 |
+
Math.sin((x + y) * 0.038) * 1.8 +
|
| 556 |
+
Math.cos(x * 0.042) * Math.sin(y * 0.035) * 1.2;
|
| 557 |
+
|
| 558 |
+
// Create valley near runway (smoothed area)
|
| 559 |
+
const distToRunway = Math.sqrt(x * x + y * y);
|
| 560 |
+
const valleyFactor = Math.max(0, 1 - distToRunway / 400);
|
| 561 |
+
const valleySmoothing = valleyFactor * valleyFactor * 30;
|
| 562 |
+
|
| 563 |
+
return continental + ridges + hills + details - valleySmoothing;
|
| 564 |
}
|
| 565 |
|
| 566 |
createSky() {
|
|
|
|
| 758 |
|
| 759 |
createDistantMountains() {
|
| 760 |
const mountainGroup = new THREE.Group();
|
| 761 |
+
|
| 762 |
+
// Varied rock materials for different mountain types
|
| 763 |
+
const darkRockMaterial = new THREE.MeshStandardMaterial({
|
| 764 |
+
color: 0x3a3540,
|
| 765 |
+
roughness: 0.92,
|
| 766 |
+
metalness: 0.03,
|
| 767 |
+
flatShading: true
|
| 768 |
+
});
|
| 769 |
+
const mediumRockMaterial = new THREE.MeshStandardMaterial({
|
| 770 |
+
color: 0x5a5560,
|
| 771 |
+
roughness: 0.88,
|
| 772 |
+
metalness: 0.04,
|
| 773 |
+
flatShading: true
|
| 774 |
+
});
|
| 775 |
+
const lightRockMaterial = new THREE.MeshStandardMaterial({
|
| 776 |
+
color: 0x7a7580,
|
| 777 |
+
roughness: 0.85,
|
| 778 |
+
metalness: 0.05,
|
| 779 |
+
flatShading: true
|
| 780 |
+
});
|
| 781 |
+
// Snow materials for different conditions
|
| 782 |
+
const freshSnowMaterial = new THREE.MeshStandardMaterial({
|
| 783 |
+
color: 0xf0f5fa,
|
| 784 |
+
roughness: 0.7,
|
| 785 |
metalness: 0.05,
|
| 786 |
flatShading: true
|
| 787 |
});
|
| 788 |
+
const oldSnowMaterial = new THREE.MeshStandardMaterial({
|
| 789 |
+
color: 0xd0d8e0,
|
| 790 |
+
roughness: 0.8,
|
| 791 |
+
metalness: 0.03,
|
| 792 |
+
flatShading: true
|
| 793 |
+
});
|
| 794 |
const foothillMaterial = new THREE.MeshStandardMaterial({
|
| 795 |
+
color: 0x3d4f3b,
|
| 796 |
+
roughness: 0.95,
|
| 797 |
+
metalness: 0.02,
|
| 798 |
+
flatShading: true
|
| 799 |
+
});
|
| 800 |
+
const screeMaterial = new THREE.MeshStandardMaterial({
|
| 801 |
+
color: 0x6a6860,
|
| 802 |
+
roughness: 0.98,
|
| 803 |
metalness: 0.01,
|
| 804 |
flatShading: true
|
| 805 |
});
|
| 806 |
|
| 807 |
+
// Helper to create jagged mountain geometry
|
| 808 |
+
const perturbGeometry = (geometry, intensity, preserveBase = true) => {
|
| 809 |
+
const posAttribute = geometry.attributes.position;
|
| 810 |
+
const vertex = new THREE.Vector3();
|
| 811 |
+
for (let i = 0; i < posAttribute.count; i++) {
|
| 812 |
+
vertex.fromBufferAttribute(posAttribute, i);
|
| 813 |
+
if (!preserveBase || vertex.y > -0.3 * geometry.parameters.height) {
|
| 814 |
+
vertex.x += (Math.random() - 0.5) * intensity;
|
| 815 |
+
vertex.z += (Math.random() - 0.5) * intensity;
|
| 816 |
+
vertex.y += (Math.random() - 0.5) * intensity * 0.4;
|
| 817 |
+
}
|
| 818 |
+
posAttribute.setXYZ(i, vertex.x, vertex.y, vertex.z);
|
| 819 |
+
}
|
| 820 |
+
geometry.computeVertexNormals();
|
| 821 |
+
return geometry;
|
| 822 |
+
};
|
| 823 |
+
|
| 824 |
+
// Major mountain peaks - dramatic and varied
|
| 825 |
+
for (let i = 0; i < 55; i++) {
|
| 826 |
const angle = this.randomRange(0, Math.PI * 2);
|
| 827 |
+
const radius = this.randomRange(1000, 1800);
|
| 828 |
const x = Math.cos(angle) * radius;
|
| 829 |
const z = Math.sin(angle) * radius;
|
| 830 |
const ground = this.getTerrainHeight(x, z);
|
|
|
|
|
|
|
| 831 |
|
| 832 |
+
// Varied peak heights - some very tall, some medium
|
| 833 |
+
const peakHeight = this.randomRange(180, 520);
|
| 834 |
+
const baseRadius = peakHeight * this.randomRange(0.5, 0.95);
|
| 835 |
+
|
| 836 |
+
// Choose random rock material
|
| 837 |
+
const rockMats = [darkRockMaterial, mediumRockMaterial, lightRockMaterial];
|
| 838 |
+
const rockMat = rockMats[Math.floor(Math.random() * rockMats.length)];
|
| 839 |
+
|
| 840 |
+
// Create varied mountain shapes
|
| 841 |
+
let geom;
|
| 842 |
+
const shapeType = Math.random();
|
| 843 |
+
if (shapeType < 0.3) {
|
| 844 |
+
// Sharp spire
|
| 845 |
+
geom = new THREE.CylinderGeometry(0, baseRadius * 0.7, peakHeight, 6, 4);
|
| 846 |
+
geom = perturbGeometry(geom, baseRadius * 0.3);
|
| 847 |
+
} else if (shapeType < 0.6) {
|
| 848 |
+
// Classic cone
|
| 849 |
+
geom = new THREE.CylinderGeometry(0, baseRadius, peakHeight, 8, 5);
|
| 850 |
+
geom = perturbGeometry(geom, baseRadius * 0.22);
|
| 851 |
+
} else {
|
| 852 |
+
// Broad dome with jagged top
|
| 853 |
+
geom = new THREE.CylinderGeometry(baseRadius * 0.15, baseRadius * 1.2, peakHeight, 10, 6);
|
| 854 |
+
geom = perturbGeometry(geom, baseRadius * 0.35);
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
const mountain = new THREE.Mesh(geom, rockMat);
|
| 858 |
+
mountain.position.set(x, ground + peakHeight * 0.5, z);
|
| 859 |
mountain.rotation.y = this.randomRange(0, Math.PI * 2);
|
| 860 |
mountain.castShadow = true;
|
| 861 |
mountain.receiveShadow = true;
|
| 862 |
mountainGroup.add(mountain);
|
| 863 |
+
|
| 864 |
+
// Add snow cap for tall peaks
|
| 865 |
+
if (peakHeight > 300) {
|
| 866 |
+
const snowHeight = peakHeight * this.randomRange(0.25, 0.4);
|
| 867 |
+
const snowBase = baseRadius * this.randomRange(0.25, 0.4);
|
| 868 |
+
const snowMat = Math.random() > 0.5 ? freshSnowMaterial : oldSnowMaterial;
|
| 869 |
+
|
| 870 |
+
let capGeom;
|
| 871 |
+
if (Math.random() > 0.5) {
|
| 872 |
+
capGeom = new THREE.CylinderGeometry(0, snowBase, snowHeight, 7, 3);
|
| 873 |
+
} else {
|
| 874 |
+
capGeom = new THREE.SphereGeometry(snowBase, 8, 6, 0, Math.PI * 2, 0, Math.PI * 0.6);
|
| 875 |
+
}
|
| 876 |
+
capGeom = perturbGeometry(capGeom, snowBase * 0.15, false);
|
| 877 |
+
|
| 878 |
+
const cap = new THREE.Mesh(capGeom, snowMat);
|
| 879 |
+
cap.position.set(0, peakHeight * 0.5 - snowHeight * 0.4, 0);
|
| 880 |
+
cap.rotation.y = this.randomRange(0, Math.PI * 2);
|
| 881 |
+
mountain.add(cap);
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
// Add rocky outcrops on some mountains
|
| 885 |
+
if (Math.random() > 0.6) {
|
| 886 |
+
const outcropCount = Math.floor(this.randomRange(1, 4));
|
| 887 |
+
for (let j = 0; j < outcropCount; j++) {
|
| 888 |
+
const outcropGeom = new THREE.ConeGeometry(
|
| 889 |
+
baseRadius * this.randomRange(0.1, 0.25),
|
| 890 |
+
peakHeight * this.randomRange(0.15, 0.35),
|
| 891 |
+
5
|
| 892 |
+
);
|
| 893 |
+
perturbGeometry(outcropGeom, baseRadius * 0.1, false);
|
| 894 |
+
const outcrop = new THREE.Mesh(outcropGeom, darkRockMaterial);
|
| 895 |
+
outcrop.position.set(
|
| 896 |
+
this.randomRange(-baseRadius * 0.5, baseRadius * 0.5),
|
| 897 |
+
peakHeight * this.randomRange(0.1, 0.4),
|
| 898 |
+
this.randomRange(-baseRadius * 0.5, baseRadius * 0.5)
|
| 899 |
+
);
|
| 900 |
+
mountain.add(outcrop);
|
| 901 |
+
}
|
| 902 |
+
}
|
| 903 |
}
|
| 904 |
|
| 905 |
+
// Rocky foothills - varied shapes
|
| 906 |
+
for (let i = 0; i < 100; i++) {
|
| 907 |
const angle = this.randomRange(0, Math.PI * 2);
|
| 908 |
+
const radius = this.randomRange(600, 1400);
|
| 909 |
const x = Math.cos(angle) * radius;
|
| 910 |
const z = Math.sin(angle) * radius;
|
| 911 |
const ground = this.getTerrainHeight(x, z);
|
|
|
|
|
|
|
| 912 |
|
| 913 |
+
const hillHeight = this.randomRange(50, 160);
|
| 914 |
+
const hillRadius = hillHeight * this.randomRange(1.0, 2.2);
|
| 915 |
+
|
| 916 |
+
let geom;
|
| 917 |
+
const hillType = Math.random();
|
| 918 |
+
if (hillType < 0.4) {
|
| 919 |
+
geom = new THREE.CylinderGeometry(0, hillRadius, hillHeight, 7, 3);
|
| 920 |
+
} else if (hillType < 0.7) {
|
| 921 |
+
geom = new THREE.DodecahedronGeometry(hillRadius * 0.6, 1);
|
| 922 |
+
geom.scale(1, hillHeight / (hillRadius * 0.6), 1);
|
| 923 |
+
} else {
|
| 924 |
+
geom = new THREE.ConeGeometry(hillRadius * 0.8, hillHeight, 6);
|
| 925 |
+
}
|
| 926 |
+
geom = perturbGeometry(geom, hillRadius * 0.12);
|
| 927 |
+
|
| 928 |
+
const hill = new THREE.Mesh(geom, Math.random() > 0.5 ? foothillMaterial : screeMaterial);
|
| 929 |
+
hill.position.set(x, ground + hillHeight * 0.4, z);
|
| 930 |
hill.rotation.y = this.randomRange(0, Math.PI * 2);
|
| 931 |
hill.castShadow = true;
|
| 932 |
hill.receiveShadow = true;
|
| 933 |
mountainGroup.add(hill);
|
| 934 |
}
|
| 935 |
|
| 936 |
+
// Small rocky outcrops scattered around
|
| 937 |
+
for (let i = 0; i < 80; i++) {
|
| 938 |
+
const angle = this.randomRange(0, Math.PI * 2);
|
| 939 |
+
const radius = this.randomRange(400, 1200);
|
| 940 |
+
const x = Math.cos(angle) * radius;
|
| 941 |
+
const z = Math.sin(angle) * radius;
|
| 942 |
+
const ground = this.getTerrainHeight(x, z);
|
| 943 |
+
|
| 944 |
+
if (ground < 5) continue; // Skip water areas
|
| 945 |
+
|
| 946 |
+
const rockSize = this.randomRange(8, 25);
|
| 947 |
+
const geom = new THREE.DodecahedronGeometry(rockSize, 0);
|
| 948 |
+
perturbGeometry(geom, rockSize * 0.2, false);
|
| 949 |
+
|
| 950 |
+
const rock = new THREE.Mesh(geom, darkRockMaterial);
|
| 951 |
+
rock.position.set(x, ground + rockSize * 0.3, z);
|
| 952 |
+
rock.rotation.set(
|
| 953 |
+
this.randomRange(-0.3, 0.3),
|
| 954 |
+
this.randomRange(0, Math.PI * 2),
|
| 955 |
+
this.randomRange(-0.3, 0.3)
|
| 956 |
+
);
|
| 957 |
+
rock.scale.y = this.randomRange(0.5, 1.2);
|
| 958 |
+
rock.castShadow = true;
|
| 959 |
+
rock.receiveShadow = true;
|
| 960 |
+
mountainGroup.add(rock);
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
this.scene.add(mountainGroup);
|
| 964 |
}
|
| 965 |
|
| 966 |
createForest() {
|
| 967 |
+
const maxTrees = 500;
|
| 968 |
+
const trunkGeometry = new THREE.CylinderGeometry(0.12, 0.26, 2.2, 6);
|
| 969 |
+
const canopyGeometry = new THREE.ConeGeometry(1.15, 4.2, 8);
|
| 970 |
+
const broadCanopyGeometry = new THREE.SphereGeometry(2.2, 8, 6);
|
| 971 |
+
|
| 972 |
const trunkMaterial = new THREE.MeshStandardMaterial({
|
| 973 |
color: 0x3d2719,
|
| 974 |
roughness: 0.98,
|
| 975 |
metalness: 0,
|
| 976 |
flatShading: true
|
| 977 |
});
|
| 978 |
+
// Varied canopy colors
|
| 979 |
+
const canopyMaterials = [
|
| 980 |
+
new THREE.MeshStandardMaterial({ color: 0x1a4520, roughness: 0.93, metalness: 0.01, flatShading: true }),
|
| 981 |
+
new THREE.MeshStandardMaterial({ color: 0x2d5530, roughness: 0.93, metalness: 0.01, flatShading: true }),
|
| 982 |
+
new THREE.MeshStandardMaterial({ color: 0x1f3825, roughness: 0.93, metalness: 0.01, flatShading: true }),
|
| 983 |
+
new THREE.MeshStandardMaterial({ color: 0x3a6035, roughness: 0.93, metalness: 0.01, flatShading: true }),
|
| 984 |
+
];
|
| 985 |
|
| 986 |
const trunks = new THREE.InstancedMesh(trunkGeometry, trunkMaterial, maxTrees);
|
| 987 |
+
const canopies = new THREE.InstancedMesh(canopyGeometry, canopyMaterials[0], maxTrees);
|
| 988 |
+
const broadCanopies = new THREE.InstancedMesh(broadCanopyGeometry, canopyMaterials[1], maxTrees);
|
| 989 |
trunks.castShadow = true;
|
| 990 |
trunks.receiveShadow = true;
|
| 991 |
canopies.castShadow = true;
|
| 992 |
canopies.receiveShadow = true;
|
| 993 |
+
broadCanopies.castShadow = true;
|
| 994 |
+
broadCanopies.receiveShadow = true;
|
| 995 |
|
| 996 |
const dummy = new THREE.Object3D();
|
| 997 |
|
| 998 |
+
let placedConifers = 0;
|
| 999 |
+
let placedBroad = 0;
|
| 1000 |
let attempts = 0;
|
| 1001 |
+
while (placedConifers + placedBroad < maxTrees && attempts < maxTrees * 15) {
|
| 1002 |
attempts += 1;
|
| 1003 |
+
const x = this.randomRange(-1300, 1300);
|
| 1004 |
+
const z = this.randomRange(-1300, 1300);
|
| 1005 |
+
if (Math.sqrt(x * x + z * z) < 250) continue;
|
| 1006 |
|
| 1007 |
const height = this.getTerrainHeight(x, z);
|
| 1008 |
+
if (height < 2 || height > 60) continue;
|
| 1009 |
+
|
| 1010 |
+
// Conifers prefer higher elevations, broadleaf prefer lower
|
| 1011 |
+
const isConifer = height > 25 || Math.random() > 0.4;
|
| 1012 |
+
const scale = this.randomRange(0.6, 1.8) * (isConifer ? 1.1 : 0.9);
|
| 1013 |
+
|
| 1014 |
+
if (isConifer) {
|
| 1015 |
+
// Conifer tree
|
| 1016 |
+
dummy.position.set(x, height + (2.2 * scale) / 2, z);
|
| 1017 |
+
dummy.rotation.set(0, this.randomRange(0, Math.PI * 2), 0);
|
| 1018 |
+
dummy.scale.set(scale, scale, scale);
|
| 1019 |
+
dummy.updateMatrix();
|
| 1020 |
+
trunks.setMatrixAt(placedConifers, dummy.matrix);
|
| 1021 |
+
|
| 1022 |
+
dummy.position.set(x, height + 2.6 * scale, z);
|
| 1023 |
+
dummy.scale.set(scale * 1.1, scale * 1.2, scale * 1.1);
|
| 1024 |
+
dummy.updateMatrix();
|
| 1025 |
+
canopies.setMatrixAt(placedConifers, dummy.matrix);
|
| 1026 |
+
placedConifers++;
|
| 1027 |
+
} else {
|
| 1028 |
+
// Broadleaf tree
|
| 1029 |
+
dummy.position.set(x, height + (2.2 * scale) / 2, z);
|
| 1030 |
+
dummy.rotation.set(0, this.randomRange(0, Math.PI * 2), 0);
|
| 1031 |
+
dummy.scale.set(scale * 0.8, scale, scale * 0.8);
|
| 1032 |
+
dummy.updateMatrix();
|
| 1033 |
+
trunks.setMatrixAt(maxTrees - 1 - placedBroad, dummy.matrix);
|
| 1034 |
+
|
| 1035 |
+
dummy.position.set(x, height + 2.8 * scale, z);
|
| 1036 |
+
dummy.scale.set(scale * 0.9, scale * 0.7, scale * 0.9);
|
| 1037 |
+
dummy.updateMatrix();
|
| 1038 |
+
broadCanopies.setMatrixAt(placedBroad, dummy.matrix);
|
| 1039 |
+
placedBroad++;
|
| 1040 |
+
}
|
| 1041 |
}
|
| 1042 |
|
| 1043 |
+
trunks.count = placedConifers + placedBroad;
|
| 1044 |
+
canopies.count = placedConifers;
|
| 1045 |
+
broadCanopies.count = placedBroad;
|
| 1046 |
trunks.instanceMatrix.needsUpdate = true;
|
| 1047 |
canopies.instanceMatrix.needsUpdate = true;
|
| 1048 |
+
broadCanopies.instanceMatrix.needsUpdate = true;
|
| 1049 |
this.scene.add(trunks);
|
| 1050 |
this.scene.add(canopies);
|
| 1051 |
+
this.scene.add(broadCanopies);
|
| 1052 |
}
|
| 1053 |
|
| 1054 |
createLake() {
|
| 1055 |
+
// Find the lowest spot for the lake
|
| 1056 |
+
let lowestSpot = { x: 400, z: -450, h: this.getTerrainHeight(400, -450) };
|
| 1057 |
+
for (let i = 0; i < 300; i++) {
|
| 1058 |
+
const x = this.randomRange(-1000, 1000);
|
| 1059 |
+
const z = this.randomRange(-1000, 1000);
|
| 1060 |
+
if (Math.sqrt(x * x + z * z) < 280) continue;
|
| 1061 |
const h = this.getTerrainHeight(x, z);
|
| 1062 |
if (h < lowestSpot.h) {
|
| 1063 |
lowestSpot = { x, z, h };
|
| 1064 |
}
|
| 1065 |
}
|
| 1066 |
|
| 1067 |
+
const lakeRadius = 180;
|
| 1068 |
+
const waterLevel = lowestSpot.h + 1.2;
|
| 1069 |
|
| 1070 |
+
// Main lake water with reflective material
|
| 1071 |
const lakeMaterial = new THREE.MeshStandardMaterial({
|
| 1072 |
+
color: 0x4a8090,
|
| 1073 |
+
emissive: 0x1a3040,
|
| 1074 |
+
emissiveIntensity: 0.15,
|
| 1075 |
+
roughness: 0.08,
|
| 1076 |
+
metalness: 0.9,
|
| 1077 |
transparent: true,
|
| 1078 |
+
opacity: 0.88,
|
| 1079 |
flatShading: true
|
| 1080 |
});
|
| 1081 |
|
| 1082 |
const lake = new THREE.Mesh(
|
| 1083 |
+
new THREE.CircleGeometry(lakeRadius, 64),
|
| 1084 |
lakeMaterial
|
| 1085 |
);
|
| 1086 |
lake.rotation.x = -Math.PI / 2;
|
|
|
|
| 1088 |
lake.receiveShadow = true;
|
| 1089 |
this.scene.add(lake);
|
| 1090 |
|
| 1091 |
+
// Lake shore with beach
|
| 1092 |
const shore = new THREE.Mesh(
|
| 1093 |
+
new THREE.RingGeometry(lakeRadius, lakeRadius + 18, 64),
|
| 1094 |
new THREE.MeshStandardMaterial({
|
| 1095 |
+
color: 0x6a7a5a,
|
| 1096 |
+
roughness: 0.92,
|
| 1097 |
metalness: 0,
|
| 1098 |
flatShading: true
|
| 1099 |
})
|
| 1100 |
);
|
| 1101 |
shore.rotation.x = -Math.PI / 2;
|
| 1102 |
+
shore.position.set(lowestSpot.x, waterLevel - 0.02, lowestSpot.z);
|
| 1103 |
shore.receiveShadow = true;
|
| 1104 |
this.scene.add(shore);
|
| 1105 |
+
|
| 1106 |
+
// Create rivers flowing into the lake from higher elevations
|
| 1107 |
+
const riverCount = 3;
|
| 1108 |
+
const riverMaterial = new THREE.MeshStandardMaterial({
|
| 1109 |
+
color: 0x3a7080,
|
| 1110 |
+
roughness: 0.15,
|
| 1111 |
+
metalness: 0.7,
|
| 1112 |
+
transparent: true,
|
| 1113 |
+
opacity: 0.85,
|
| 1114 |
+
flatShading: true
|
| 1115 |
+
});
|
| 1116 |
+
|
| 1117 |
+
for (let r = 0; r < riverCount; r++) {
|
| 1118 |
+
// Start river from a random high point
|
| 1119 |
+
const startAngle = (r / riverCount) * Math.PI * 2 + this.randomRange(-0.5, 0.5);
|
| 1120 |
+
const startRadius = this.randomRange(600, 900);
|
| 1121 |
+
let currentX = Math.cos(startAngle) * startRadius;
|
| 1122 |
+
let currentZ = Math.sin(startAngle) * startRadius;
|
| 1123 |
+
|
| 1124 |
+
// Flow towards lake
|
| 1125 |
+
const riverPoints = [];
|
| 1126 |
+
const maxPoints = 60;
|
| 1127 |
+
for (let i = 0; i < maxPoints; i++) {
|
| 1128 |
+
const height = this.getTerrainHeight(currentX, currentZ);
|
| 1129 |
+
riverPoints.push(new THREE.Vector3(currentX, height + 0.3, currentZ));
|
| 1130 |
+
|
| 1131 |
+
// Move towards lake with some wandering
|
| 1132 |
+
const toLakeX = lowestSpot.x - currentX;
|
| 1133 |
+
const toLakeZ = lowestSpot.z - currentZ;
|
| 1134 |
+
const distToLake = Math.sqrt(toLakeX * toLakeX + toLakeZ * toLakeZ);
|
| 1135 |
+
|
| 1136 |
+
if (distToLake < lakeRadius + 20) break;
|
| 1137 |
+
|
| 1138 |
+
// Flow downhill with some meandering
|
| 1139 |
+
const wander = this.randomRange(-0.4, 0.4);
|
| 1140 |
+
currentX += (toLakeX / distToLake) * 25 + wander * 15;
|
| 1141 |
+
currentZ += (toLakeZ / distToLake) * 25 + wander * 15;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
// Create river as a tube
|
| 1145 |
+
if (riverPoints.length >= 2) {
|
| 1146 |
+
const curve = new THREE.CatmullRomCurve3(riverPoints);
|
| 1147 |
+
const riverWidth = this.randomRange(8, 14);
|
| 1148 |
+
const tubeGeom = new THREE.TubeGeometry(curve, riverPoints.length * 2, riverWidth, 6, false);
|
| 1149 |
+
const river = new THREE.Mesh(tubeGeom, riverMaterial);
|
| 1150 |
+
river.receiveShadow = true;
|
| 1151 |
+
this.scene.add(river);
|
| 1152 |
+
}
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
// Store lake info for other systems
|
| 1156 |
+
this.lakePosition = lowestSpot;
|
| 1157 |
+
this.lakeRadius = lakeRadius;
|
| 1158 |
+
this.lakeWaterLevel = waterLevel;
|
| 1159 |
}
|
| 1160 |
|
| 1161 |
randomRange(min, max) {
|