From 33fcc4c74ab350fcbd5a436e93325b5e1e1b53f9 Mon Sep 17 00:00:00 2001 From: como Date: Thu, 26 Mar 2026 11:27:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=88=E6=9E=9C=E4=B8=80=E8=88=AC=E7=9A=84?= =?UTF-8?q?=E9=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 15 +++ src/OceanScene.js | 287 +++++++++++++++++++++++++++++++++++++++++++++- src/main.js | 3 + 3 files changed, 303 insertions(+), 2 deletions(-) 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);