underrate commited on
Commit
5034610
·
verified ·
1 Parent(s): 69f4122

feat: enhance terrain generation with varied biomes, improved mountain shapes, and dynamic river systems

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. src/Aircraft.js +27 -23
  3. 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**: Flat-shaded terrain, mountains, 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)
 
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 (Simulator focused)
264
  if (document.pointerLockElement === document.body) {
265
- const mouseSensX = 0.005;
266
- const mouseSensY = 0.005;
267
-
268
- // Map mouse Y to Pitch: inverted movementY so mouse Up = plane nose Up
269
- this.pitch = THREE.MathUtils.clamp(this.pitch - event.movementY * mouseSensY, -1, 1);
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
- // Update rotation using rates
 
 
 
 
 
 
 
 
 
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
- // Apply drag to the sticks so they recenter automatically if mouse stops moving
334
- if (document.pointerLockElement === document.body) {
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 logic
344
- if (Math.abs(this.roll) < 0.05) {
345
- this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, 0, deltaTime * 0.8);
346
  }
347
- if (Math.abs(this.pitch) < 0.05) {
348
- this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, 0, deltaTime * 0.3);
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 = 2800;
392
- const segments = 260;
393
  const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
394
 
395
  const position = geometry.attributes.position;
396
  const colors = [];
397
- const lowlandColor = new THREE.Color(0x1a2e1d);
398
- const grassColor = new THREE.Color(0x264d28);
399
- const highlandColor = new THREE.Color(0x384a2f);
400
- const alpineColor = new THREE.Color(0x6a6a60);
401
- const waterEdgeColor = new THREE.Color(0x152820);
 
 
 
 
 
 
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
- const sampleStep = 5;
 
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 / 28, 0, 1);
420
- const ridgeNoise =
421
- Math.sin(x * 0.022 + y * 0.009) +
422
- Math.cos(y * 0.018) +
423
- Math.sin((x - y) * 0.013);
424
- const variation = ridgeNoise * 0.016;
425
-
426
- if (height < -4) {
427
- color.copy(waterEdgeColor);
428
- } else if (height < 4) {
429
- blend.lerpColors(lowlandColor, grassColor, THREE.MathUtils.clamp((height + 4) / 8, 0, 1));
 
 
 
 
 
 
 
 
 
430
  color.copy(blend);
431
- } else if (height < 18) {
432
- blend.lerpColors(grassColor, highlandColor, THREE.MathUtils.clamp((height - 4) / 14, 0, 1));
 
 
 
 
 
 
433
  color.copy(blend);
434
  } else {
435
- blend.lerpColors(highlandColor, alpineColor, THREE.MathUtils.clamp((height - 18) / 28, 0, 1));
436
  color.copy(blend);
437
  }
438
 
439
- color.lerp(alpineColor, slopeFactor * 0.42);
 
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.0016) * 15 +
514
- Math.cos(y * 0.00145) * 13;
 
 
 
515
  const ridges =
516
- Math.sin((x + y) * 0.0038) * 9 +
517
- Math.cos((x - y) * 0.0027) * 7;
 
 
 
 
 
 
 
 
518
  const details =
519
- Math.sin(x * 0.013) * Math.cos(y * 0.011) * 3 +
520
- Math.sin((x + y) * 0.023) * 1.4;
521
- return continental + ridges + details;
 
 
 
 
 
 
 
522
  }
523
 
524
  createSky() {
@@ -716,152 +758,329 @@ class FlightSimulator {
716
 
717
  createDistantMountains() {
718
  const mountainGroup = new THREE.Group();
719
- const rockMaterial = new THREE.MeshStandardMaterial({
720
- color: 0x5a5b50,
721
- roughness: 0.95,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
722
  metalness: 0.05,
723
  flatShading: true
724
  });
 
 
 
 
 
 
725
  const foothillMaterial = new THREE.MeshStandardMaterial({
726
- color: 0x4a5d3f,
727
- roughness: 0.96,
 
 
 
 
 
 
728
  metalness: 0.01,
729
  flatShading: true
730
  });
731
 
732
- for (let i = 0; i < 42; i++) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  const angle = this.randomRange(0, Math.PI * 2);
734
- const radius = this.randomRange(900, 1500);
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
- const mountain = new THREE.Mesh(
742
- new THREE.ConeGeometry(baseRadius, peakHeight, 8),
743
- rockMaterial
744
- );
745
- mountain.position.set(x, ground + peakHeight * 0.46, z);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- for (let i = 0; i < 70; i++) {
 
753
  const angle = this.randomRange(0, Math.PI * 2);
754
- const radius = this.randomRange(620, 1300);
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 hill = new THREE.Mesh(
762
- new THREE.ConeGeometry(hillRadius, hillHeight, 7),
763
- foothillMaterial
764
- );
765
- hill.position.set(x, ground + hillHeight * 0.44, z);
 
 
 
 
 
 
 
 
 
 
 
 
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 = 360;
777
- const trunkGeometry = new THREE.CylinderGeometry(0.15, 0.28, 2.4, 6);
778
- const canopyGeometry = new THREE.ConeGeometry(1.25, 4.8, 8);
 
 
779
  const trunkMaterial = new THREE.MeshStandardMaterial({
780
  color: 0x3d2719,
781
  roughness: 0.98,
782
  metalness: 0,
783
  flatShading: true
784
  });
785
- const canopyMaterial = new THREE.MeshStandardMaterial({
786
- color: 0x1f4225,
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, canopyMaterial, maxTrees);
 
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 placed = 0;
 
802
  let attempts = 0;
803
- while (placed < maxTrees && attempts < maxTrees * 12) {
804
  attempts += 1;
805
- const x = this.randomRange(-1180, 1180);
806
- const z = this.randomRange(-1180, 1180);
807
- if (Math.sqrt(x * x + z * z) < 230) continue;
808
 
809
  const height = this.getTerrainHeight(x, z);
810
- if (height < 2 || height > 34) continue;
811
- if (Math.random() < 0.3 && height > 22) continue;
812
-
813
- const scale = this.randomRange(0.75, 1.75);
814
- dummy.position.set(x, height + (2.4 * scale) / 2, z);
815
- dummy.rotation.set(0, this.randomRange(0, Math.PI * 2), 0);
816
- dummy.scale.set(scale, scale, scale);
817
- dummy.updateMatrix();
818
- trunks.setMatrixAt(placed, dummy.matrix);
819
-
820
- dummy.position.set(x, height + 2.8 * scale, z);
821
- dummy.rotation.set(0, this.randomRange(0, Math.PI * 2), 0);
822
- dummy.scale.set(scale * 1.18, scale * 1.24, scale * 1.18);
823
- dummy.updateMatrix();
824
- canopies.setMatrixAt(placed, dummy.matrix);
825
- placed += 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  }
827
 
828
- trunks.count = placed;
829
- canopies.count = placed;
 
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
- let lowestSpot = { x: 360, z: -420, h: this.getTerrainHeight(360, -420) };
838
- for (let i = 0; i < 260; i++) {
839
- const x = this.randomRange(-900, 900);
840
- const z = this.randomRange(-900, 900);
841
- if (Math.sqrt(x * x + z * z) < 260) continue;
 
842
  const h = this.getTerrainHeight(x, z);
843
  if (h < lowestSpot.h) {
844
  lowestSpot = { x, z, h };
845
  }
846
  }
847
 
848
- const lakeRadius = 160;
849
- const waterLevel = lowestSpot.h + 0.55;
850
 
851
- // Highly reflective premium water material matching sunset
852
  const lakeMaterial = new THREE.MeshStandardMaterial({
853
- color: 0xff7766,
854
- emissive: 0x5a1835,
855
- emissiveIntensity: 0.2,
856
- roughness: 0.05,
857
- metalness: 0.95,
858
  transparent: true,
859
- opacity: 0.85,
860
  flatShading: true
861
  });
862
 
863
  const lake = new THREE.Mesh(
864
- new THREE.CircleGeometry(lakeRadius, 72),
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 + 12, 72),
874
  new THREE.MeshStandardMaterial({
875
- color: 0x4a5d3f,
876
- roughness: 0.95,
877
  metalness: 0,
878
  flatShading: true
879
  })
880
  );
881
  shore.rotation.x = -Math.PI / 2;
882
- shore.position.set(lowestSpot.x, waterLevel - 0.01, lowestSpot.z);
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) {