diff --git a/index.html b/index.html
index e3ea4ec..1a75e26 100644
--- a/index.html
+++ b/index.html
@@ -222,6 +222,21 @@
0.50
+
+
+
+
0.42
+
+
+
+
+
0.32
+
+
+
+
+
0.55
+
FPS: 60
diff --git a/src/OceanScene.js b/src/OceanScene.js
index c9462ea..670a535 100644
--- a/src/OceanScene.js
+++ b/src/OceanScene.js
@@ -30,6 +30,10 @@ export class OceanScene {
this.cloudGroup = null;
this.cloudMaterials = [];
this.cloudLayers = [];
+ this.fogGroup = null;
+ this.fogLayers = [];
+ this.horizonFog = null;
+ this.skyHazeBand = null;
this.params = {
elevation: 2,
@@ -43,6 +47,9 @@ export class OceanScene {
cloudCoverage: 0.4,
cloudDensity: 0.5,
cloudElevation: 0.5,
+ fogDensity: 0.42,
+ fogHeight: 0.32,
+ fogRange: 0.55,
mieCoefficient: 0.005,
mieDirectionalG: 0.8
};
@@ -61,6 +68,7 @@ export class OceanScene {
this.initPostProcessing();
await this.initSky();
this.initClouds();
+ this.initFog();
await this.initWater();
await this.initTerrain();
await this.initVegetation();
@@ -84,7 +92,7 @@ export class OceanScene {
initScene() {
this.scene = new THREE.Scene();
- this.scene.fog = new THREE.FogExp2(0x8cb8d4, 0.0008);
+ this.scene.fog = new THREE.FogExp2(0x8cb8d4, 0.0006);
}
initCamera() {
@@ -225,6 +233,62 @@ export class OceanScene {
this.cloudGroup.add(mesh);
}
+ initFog() {
+ const fogTexture = this.createFogTexture();
+ this.fogGroup = new THREE.Group();
+ this.fogLayers = [];
+
+ const layerConfigs = [
+ { width: 4600, height: 2400, y: 8, opacity: 0.26, speedX: 0.00055, speedY: 0.0001, rotation: 0.08 },
+ { width: 3900, height: 1900, y: 22, opacity: 0.2, speedX: -0.00032, speedY: 0.00014, rotation: -0.05 },
+ { width: 3200, height: 1500, y: 42, opacity: 0.13, speedX: 0.00024, speedY: -0.00008, rotation: 0.12 }
+ ];
+
+ layerConfigs.forEach((config) => {
+ const texture = fogTexture.clone();
+ texture.wrapS = THREE.RepeatWrapping;
+ texture.wrapT = THREE.RepeatWrapping;
+ texture.repeat.set(2.4, 1.4);
+ texture.needsUpdate = true;
+
+ const material = new THREE.MeshBasicMaterial({
+ map: texture,
+ alphaMap: texture,
+ color: 0xdbe7ef,
+ transparent: true,
+ opacity: config.opacity,
+ depthWrite: false,
+ fog: false,
+ side: THREE.DoubleSide,
+ blending: THREE.NormalBlending
+ });
+
+ const geometry = new THREE.PlaneGeometry(config.width, config.height, 1, 1);
+ const mesh = new THREE.Mesh(geometry, material);
+ mesh.rotation.x = -Math.PI / 2;
+ mesh.rotation.z = config.rotation;
+ mesh.position.y = config.y;
+
+ this.fogLayers.push({
+ mesh,
+ texture,
+ baseY: config.y,
+ baseOpacity: config.opacity,
+ speedX: config.speedX,
+ speedY: config.speedY
+ });
+
+ this.fogGroup.add(mesh);
+ });
+
+ this.horizonFog = this.createHorizonFog();
+ this.fogGroup.add(this.horizonFog);
+ this.skyHazeBand = this.createSkyHazeBand();
+ this.fogGroup.add(this.skyHazeBand);
+ this.scene.add(this.fogGroup);
+ this.updateFog();
+ }
+
createCloudTexture() {
const canvas = document.createElement('canvas');
canvas.width = 256;
@@ -254,6 +318,129 @@ export class OceanScene {
return texture;
}
+
+ createFogTexture() {
+ const canvas = document.createElement('canvas');
+ canvas.width = 512;
+ canvas.height = 512;
+
+ const context = canvas.getContext('2d');
+ context.clearRect(0, 0, canvas.width, canvas.height);
+
+ for (let i = 0; i < 42; i++) {
+ const x = 30 + Math.random() * 452;
+ const y = 40 + Math.random() * 432;
+ const radius = 34 + Math.random() * 88;
+ const gradient = context.createRadialGradient(x, y, 0, x, y, radius);
+
+ gradient.addColorStop(0, 'rgba(255,255,255,0.82)');
+ gradient.addColorStop(0.22, 'rgba(255,255,255,0.58)');
+ gradient.addColorStop(0.58, 'rgba(255,255,255,0.16)');
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
+
+ context.fillStyle = gradient;
+ context.fillRect(x - radius, y - radius, radius * 2, radius * 2);
+ }
+
+ const texture = new THREE.CanvasTexture(canvas);
+ texture.colorSpace = THREE.SRGBColorSpace;
+ texture.needsUpdate = true;
+
+ return texture;
+ }
+
+ createHorizonFogTexture() {
+ const canvas = document.createElement('canvas');
+ canvas.width = 64;
+ canvas.height = 512;
+
+ const context = canvas.getContext('2d');
+ const gradient = context.createLinearGradient(0, 0, 0, canvas.height);
+ gradient.addColorStop(0, 'rgba(255,255,255,0)');
+ gradient.addColorStop(0.16, 'rgba(255,255,255,0.08)');
+ gradient.addColorStop(0.38, 'rgba(255,255,255,0.62)');
+ gradient.addColorStop(0.6, 'rgba(255,255,255,0.42)');
+ gradient.addColorStop(0.82, 'rgba(255,255,255,0.06)');
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
+ context.fillStyle = gradient;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ const texture = new THREE.CanvasTexture(canvas);
+ texture.colorSpace = THREE.SRGBColorSpace;
+ texture.wrapS = THREE.RepeatWrapping;
+ texture.wrapT = THREE.ClampToEdgeWrapping;
+ texture.repeat.set(4, 1);
+ texture.needsUpdate = true;
+ return texture;
+ }
+
+ createSkyHazeTexture() {
+ const canvas = document.createElement('canvas');
+ canvas.width = 64;
+ canvas.height = 512;
+
+ const context = canvas.getContext('2d');
+ const gradient = context.createLinearGradient(0, 0, 0, canvas.height);
+ gradient.addColorStop(0, 'rgba(255,255,255,0)');
+ gradient.addColorStop(0.12, 'rgba(255,255,255,0.04)');
+ gradient.addColorStop(0.34, 'rgba(255,255,255,0.32)');
+ gradient.addColorStop(0.56, 'rgba(255,255,255,0.78)');
+ gradient.addColorStop(0.82, 'rgba(255,255,255,0.18)');
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
+ context.fillStyle = gradient;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ const texture = new THREE.CanvasTexture(canvas);
+ texture.colorSpace = THREE.SRGBColorSpace;
+ texture.wrapS = THREE.RepeatWrapping;
+ texture.wrapT = THREE.ClampToEdgeWrapping;
+ texture.repeat.set(3, 1);
+ texture.needsUpdate = true;
+ return texture;
+ }
+
+ createHorizonFog() {
+ const texture = this.createHorizonFogTexture();
+ const material = new THREE.MeshBasicMaterial({
+ map: texture,
+ alphaMap: texture,
+ color: 0xdde8f2,
+ transparent: true,
+ opacity: 0.38,
+ fog: false,
+ depthWrite: false,
+ side: THREE.BackSide
+ });
+
+ const geometry = new THREE.CylinderGeometry(4700, 4700, 900, 72, 1, true);
+ const mesh = new THREE.Mesh(geometry, material);
+ mesh.position.y = 130;
+ mesh.renderOrder = -1;
+ mesh.userData.texture = texture;
+ return mesh;
+ }
+
+ createSkyHazeBand() {
+ const texture = this.createSkyHazeTexture();
+ const material = new THREE.MeshBasicMaterial({
+ map: texture,
+ alphaMap: texture,
+ color: 0xdde8f2,
+ transparent: true,
+ opacity: 0.32,
+ fog: false,
+ depthWrite: false,
+ side: THREE.BackSide,
+ blending: THREE.NormalBlending
+ });
+
+ const geometry = new THREE.CylinderGeometry(5200, 5200, 1600, 72, 1, true);
+ const mesh = new THREE.Mesh(geometry, material);
+ mesh.position.y = 420;
+ mesh.renderOrder = -2;
+ mesh.userData.texture = texture;
+ return mesh;
+ }
async initWater() {
const waterGeometry = new THREE.PlaneGeometry(10000, 10000, 128, 128);
@@ -395,7 +582,7 @@ export class OceanScene {
this.scene.environment = this.renderTarget.texture;
this.scene.add(this.sky);
- this.scene.fog.color.setHex(this.getFogColor());
+ this.updateFog();
this.updateClouds();
}
@@ -416,6 +603,20 @@ export class OceanScene {
return 0xd4e8f4;
}
}
+
+ getAtmosphereColors() {
+ const sunMix = THREE.MathUtils.clamp((this.params.elevation + 10) / 100, 0, 1);
+ const fogColor = new THREE.Color(this.getFogColor());
+ const warmHorizon = new THREE.Color(0xf0c7a3);
+ const coolHorizon = new THREE.Color(0xcfe0ee);
+ const horizonColor = warmHorizon.clone().lerp(coolHorizon, sunMix);
+ const warmSkyBase = new THREE.Color(0xf6d7b8);
+ const coolSkyBase = new THREE.Color(0xbfd8eb);
+ const skyBaseColor = warmSkyBase.clone().lerp(coolSkyBase, sunMix * 0.92);
+ const skyBlendColor = skyBaseColor.clone().lerp(fogColor, 0.42);
+
+ return { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor };
+ }
initEventListeners() {
window.addEventListener('resize', () => this.onWindowResize());
@@ -495,6 +696,21 @@ export class OceanScene {
}
}
+ setFogDensity(value) {
+ this.params.fogDensity = value;
+ this.updateFog();
+ }
+
+ setFogHeight(value) {
+ this.params.fogHeight = value;
+ this.updateFog();
+ }
+
+ setFogRange(value) {
+ this.params.fogRange = value;
+ this.updateFog();
+ }
+
updateClouds() {
if (!this.cloudGroup) return;
@@ -512,6 +728,59 @@ export class OceanScene {
layer.mesh.visible = opacity > 0.015;
}
}
+
+ updateFog() {
+ const { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor } = this.getAtmosphereColors();
+ const fogDensity = THREE.MathUtils.lerp(0.00015, 0.0018, this.params.fogDensity);
+
+ if (this.scene.fog) {
+ this.scene.fog.color.copy(fogColor);
+ this.scene.fog.density = fogDensity * THREE.MathUtils.lerp(0.7, 1.4, this.params.fogRange);
+ }
+
+ if (!this.fogGroup) return;
+
+ const fogLayerColor = horizonColor.clone().lerp(fogColor, 0.55);
+ const heightBase = THREE.MathUtils.lerp(-20, 95, this.params.fogHeight);
+ const verticalSpread = THREE.MathUtils.lerp(20, 110, this.params.fogHeight);
+ const rangeOpacity = THREE.MathUtils.lerp(0.55, 1.35, this.params.fogRange);
+
+ this.fogLayers.forEach((layer, index) => {
+ layer.mesh.position.y = heightBase + index * verticalSpread * 0.36;
+ layer.mesh.scale.setScalar(THREE.MathUtils.lerp(0.82, 1.2, this.params.fogRange));
+ layer.mesh.material.opacity = layer.baseOpacity * THREE.MathUtils.lerp(0.18, 1.35, this.params.fogDensity) * rangeOpacity;
+ layer.mesh.material.color.copy(fogLayerColor);
+ layer.mesh.visible = layer.mesh.material.opacity > 0.01;
+ });
+
+ if (this.horizonFog) {
+ this.horizonFog.material.color.copy(horizonColor);
+ this.horizonFog.material.opacity =
+ THREE.MathUtils.lerp(0.16, 0.5, this.params.fogDensity) *
+ THREE.MathUtils.lerp(0.7, 1.28, this.params.fogRange);
+ this.horizonFog.position.y = THREE.MathUtils.lerp(70, 190, this.params.fogHeight);
+ this.horizonFog.scale.set(
+ THREE.MathUtils.lerp(0.88, 1.28, this.params.fogRange),
+ THREE.MathUtils.lerp(0.8, 1.18, this.params.fogHeight),
+ THREE.MathUtils.lerp(0.88, 1.28, this.params.fogRange)
+ );
+ this.horizonFog.visible = this.horizonFog.material.opacity > 0.01;
+ }
+
+ if (this.skyHazeBand) {
+ this.skyHazeBand.material.color.copy(skyBlendColor);
+ this.skyHazeBand.material.opacity =
+ THREE.MathUtils.lerp(0.1, 0.32, this.params.fogDensity) *
+ THREE.MathUtils.lerp(0.82, 1.18, this.params.fogRange);
+ this.skyHazeBand.position.y = THREE.MathUtils.lerp(300, 520, this.params.fogHeight);
+ this.skyHazeBand.scale.set(
+ THREE.MathUtils.lerp(0.96, 1.16, this.params.fogRange),
+ THREE.MathUtils.lerp(0.88, 1.14, this.params.fogHeight),
+ THREE.MathUtils.lerp(0.96, 1.16, this.params.fogRange)
+ );
+ this.skyHazeBand.visible = this.skyHazeBand.material.opacity > 0.01;
+ }
+ }
animate() {
requestAnimationFrame(() => this.animate());
@@ -530,6 +799,20 @@ export class OceanScene {
});
}
+ if (this.fogGroup) {
+ this.fogLayers.forEach((layer, index) => {
+ layer.texture.offset.x = time * layer.speedX;
+ layer.texture.offset.y = time * layer.speedY;
+ layer.mesh.rotation.z += Math.sin(time * 0.05 + index) * 0.00002;
+ });
+ if (this.horizonFog?.userData.texture) {
+ this.horizonFog.userData.texture.offset.x = time * 0.00035;
+ }
+ if (this.skyHazeBand?.userData.texture) {
+ this.skyHazeBand.userData.texture.offset.x = -time * 0.00018;
+ }
+ }
+
if (this.vegetationSystem) {
this.vegetationSystem.update(time);
}
diff --git a/src/main.js b/src/main.js
index 2decef1..9c48ea3 100644
--- a/src/main.js
+++ b/src/main.js
@@ -52,6 +52,9 @@ function setupControls(oceanScene) {
bindSlider('cloud-coverage', (value) => value.toFixed(2), (value) => oceanScene.setCloudCoverage(value));
bindSlider('cloud-density', (value) => value.toFixed(2), (value) => oceanScene.setCloudDensity(value));
bindSlider('cloud-elevation', (value) => value.toFixed(2), (value) => oceanScene.setCloudElevation(value));
+ bindSlider('fog-density', (value) => value.toFixed(2), (value) => oceanScene.setFogDensity(value));
+ bindSlider('fog-height', (value) => value.toFixed(2), (value) => oceanScene.setFogHeight(value));
+ bindSlider('fog-range', (value) => value.toFixed(2), (value) => oceanScene.setFogRange(value));
}
main().catch(console.error);