From 502aafc41d2e8cc94545bf886d40bd96dc6eea10 Mon Sep 17 00:00:00 2001 From: como Date: Sat, 28 Mar 2026 15:12:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E7=94=A8=E4=BD=93=E7=A7=AF=E9=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/OceanScene.js | 274 ++++++++++++++++++++++++++++++------------ src/main.js | 15 +-- src/weatherPresets.js | 1 + 3 files changed, 205 insertions(+), 85 deletions(-) diff --git a/src/OceanScene.js b/src/OceanScene.js index 26f56ee..688e186 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -43,8 +43,10 @@ export class OceanScene { this.lightningCloudGlow = null; this.composer = null; this.bloomPass = null; + this.fogPass = null; this.rainPass = null; this.snowPass = null; + this.depthTarget = null; this.stats = null; this.cloudGroup = null; this.cloudMaterials = []; @@ -87,7 +89,6 @@ export class OceanScene { this.initStars(); this.initNightSky(); this.initClouds(); - this.initFog(); await this.initWater(); await this.initTerrain(); await this.initWindTurbine(); @@ -124,7 +125,6 @@ export class OceanScene { initScene() { this.scene = new THREE.Scene(); - this.scene.fog = new THREE.FogExp2(0x8cb8d4, 0.0006); } initCamera() { @@ -179,6 +179,13 @@ export class OceanScene { } initPostProcessing() { + this.depthTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight); + this.depthTarget.depthTexture = new THREE.DepthTexture(window.innerWidth, window.innerHeight, THREE.UnsignedIntType); + this.depthTarget.depthTexture.format = THREE.DepthFormat; + this.depthTarget.texture.minFilter = THREE.NearestFilter; + this.depthTarget.texture.magFilter = THREE.NearestFilter; + this.depthTarget.texture.generateMipmaps = false; + this.composer = new EffectComposer(this.renderer); this.composer.addPass(new RenderPass(this.scene, this.camera)); @@ -190,6 +197,13 @@ export class OceanScene { ); this.composer.addPass(this.bloomPass); + this.fogPass = new ShaderPass(this.createVolumetricFogShader()); + this.fogPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); + this.fogPass.material.uniforms.tDepth.value = this.depthTarget.depthTexture; + this.fogPass.material.uniforms.cameraNear.value = this.camera.near; + this.fogPass.material.uniforms.cameraFar.value = this.camera.far; + this.composer.addPass(this.fogPass); + this.rainPass = new ShaderPass(this.createRainShader()); this.rainPass.enabled = this.params.rainEnabled; this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); @@ -455,6 +469,134 @@ export class OceanScene { }; } + createVolumetricFogShader() { + return { + uniforms: { + tDiffuse: { value: null }, + tDepth: { value: null }, + resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, + cameraNear: { value: this.camera?.near ?? 1 }, + cameraFar: { value: this.camera?.far ?? 20000 }, + projectionMatrixInverse: { value: new THREE.Matrix4() }, + viewMatrixInverse: { value: new THREE.Matrix4() }, + cameraWorldPosition: { value: new THREE.Vector3() }, + fogColor: { value: new THREE.Color(0x9ec5db) }, + horizonColor: { value: new THREE.Color(0xcfe0ee) }, + fogDensity: { value: 0.0 }, + fogHeight: { value: this.params.fogHeight }, + fogRange: { value: this.params.fogRange }, + time: { value: 0 } + }, + vertexShader: ` + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D tDiffuse; + uniform sampler2D tDepth; + uniform vec2 resolution; + uniform mat4 projectionMatrixInverse; + uniform mat4 viewMatrixInverse; + uniform vec3 cameraWorldPosition; + uniform vec3 fogColor; + uniform vec3 horizonColor; + uniform float fogDensity; + uniform float fogHeight; + uniform float fogRange; + uniform float time; + + varying vec2 vUv; + + float hash21(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); + } + + vec3 reconstructWorldPosition(vec2 uv, float depth) { + float z = depth * 2.0 - 1.0; + vec4 clipPosition = vec4(uv * 2.0 - 1.0, z, 1.0); + vec4 viewPosition = projectionMatrixInverse * clipPosition; + viewPosition /= max(viewPosition.w, 0.0001); + vec4 worldPosition = viewMatrixInverse * viewPosition; + return worldPosition.xyz; + } + + float sampleMediumDensity(vec3 samplePosition, float traveledDistance) { + float seaLevel = 2.0; + float verticalOffset = max(samplePosition.y - seaLevel, 0.0); + float heightFalloff = mix(0.08, 0.02, fogHeight); + float heightMask = exp(-verticalOffset * heightFalloff); + + float seaMask = smoothstep(210.0, -12.0, samplePosition.y); + float distanceMask = smoothstep(120.0, mix(900.0, 5400.0, fogRange), traveledDistance); + float upperFade = 1.0 - smoothstep( + mix(120.0, 260.0, fogHeight), + mix(360.0, 760.0, fogHeight), + samplePosition.y + ); + + float windNoise = hash21(samplePosition.xz * 0.0008 + vec2(time * 0.012, -time * 0.008)); + float breakup = mix(0.82, 1.14, windNoise); + + return fogDensity * heightMask * seaMask * distanceMask * upperFade * breakup; + } + + void main() { + vec3 base = texture2D(tDiffuse, vUv).rgb; + float depth = texture2D(tDepth, vUv).x; + + if (fogDensity <= 0.00001) { + gl_FragColor = vec4(base, 1.0); + return; + } + + float sceneDepth = depth < 0.99999 ? depth : 0.99999; + vec3 endPosition = reconstructWorldPosition(vUv, sceneDepth); + vec3 ray = endPosition - cameraWorldPosition; + float rayLength = length(ray); + + if (depth >= 0.99999) { + vec3 farPosition = reconstructWorldPosition(vUv, 0.99999); + ray = farPosition - cameraWorldPosition; + rayLength = min(length(ray), mix(2200.0, 6400.0, fogRange)); + } + + if (rayLength <= 0.0001) { + gl_FragColor = vec4(base, 1.0); + return; + } + + vec3 rayDirection = ray / rayLength; + const int STEP_COUNT = 12; + float stepLength = rayLength / float(STEP_COUNT); + float transmittance = 1.0; + vec3 inscattering = vec3(0.0); + + for (int i = 0; i < STEP_COUNT; i++) { + float jitter = hash21(gl_FragCoord.xy + float(i) * 13.37 + time * 24.0); + float traveled = (float(i) + 0.35 + jitter * 0.45) * stepLength; + vec3 samplePosition = cameraWorldPosition + rayDirection * traveled; + float localDensity = sampleMediumDensity(samplePosition, traveled); + + vec3 localTint = mix(horizonColor, fogColor, smoothstep(-8.0, 140.0, samplePosition.y)); + float extinction = 1.0 - exp(-localDensity * stepLength); + inscattering += transmittance * localTint * extinction; + transmittance *= exp(-localDensity * stepLength); + } + + vec3 color = base * transmittance + inscattering; + + gl_FragColor = vec4(color, 1.0); + } + ` + }; + } + createSnowShader() { return { uniforms: { @@ -1484,7 +1626,6 @@ export class OceanScene { this.scene.environment = this.renderTarget.texture; this.scene.add(this.sky); - this.updateFog(); this.updateClouds(); this.updateStars(); } @@ -1535,9 +1676,17 @@ export class OceanScene { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); + if (this.depthTarget) { + this.depthTarget.setSize(window.innerWidth, window.innerHeight); + } if (this.composer) { this.composer.setSize(window.innerWidth, window.innerHeight); } + if (this.fogPass) { + this.fogPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); + this.fogPass.material.uniforms.cameraNear.value = this.camera.near; + this.fogPass.material.uniforms.cameraFar.value = this.camera.far; + } if (this.rainPass) { this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); } @@ -1618,6 +1767,11 @@ export class OceanScene { } } + setFogEnabled(value) { + this.params.fogEnabled = value; + this.updateFog(); + } + setFogDensity(value) { this.params.fogDensity = value; this.updateFog(); @@ -1823,6 +1977,7 @@ export class OceanScene { this.setCloudCoverage(mergedParams.cloudCoverage); this.setCloudDensity(mergedParams.cloudDensity); this.setCloudElevation(mergedParams.cloudElevation); + this.setFogEnabled(mergedParams.fogEnabled ?? true); this.setFogDensity(mergedParams.fogDensity); this.setFogHeight(mergedParams.fogHeight); this.setFogRange(mergedParams.fogRange); @@ -2088,75 +2243,51 @@ export class OceanScene { } updateFog() { - const { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor } = this.getAtmosphereColors(); - const fogDensity = THREE.MathUtils.lerp(0.00015, 0.0018, this.params.fogDensity); + if (!this.fogPass) return; + + const { fogColor, horizonColor } = this.getAtmosphereColors(); const lightningMix = THREE.MathUtils.clamp(this.lightningFlash * 0.42, 0, 1); fogColor.lerp(new THREE.Color(0xdbe8f5), lightningMix); horizonColor.lerp(new THREE.Color(0xe5eef9), lightningMix); - skyBlendColor.lerp(new THREE.Color(0xdbe7f3), lightningMix); - 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); - } + const uniforms = this.fogPass.material.uniforms; + uniforms.fogColor.value.copy(fogColor); + uniforms.horizonColor.value.copy(horizonColor); + uniforms.fogDensity.value = this.params.fogEnabled + ? THREE.MathUtils.lerp(0.00002, 0.00042, this.params.fogDensity) + : 0.0; + uniforms.fogHeight.value = this.params.fogHeight; + uniforms.fogRange.value = this.params.fogRange; + uniforms.projectionMatrixInverse.value.copy(this.camera.projectionMatrixInverse); + uniforms.viewMatrixInverse.value.copy(this.camera.matrixWorld); + uniforms.cameraWorldPosition.value.copy(this.camera.position); + } - if (!this.fogGroup) return; + updateDepthTarget() { + if (!this.depthTarget) 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); - const nearSeaMist = THREE.MathUtils.lerp(0.12, 0.9, this.params.fogDensity) * THREE.MathUtils.lerp(0.8, 1.22, this.params.fogRange); + const hiddenObjects = [ + this.sky, + this.starField, + this.moonSprite, + this.moonGlowSprite, + this.galaxyBand + ].filter(Boolean); + const previousVisibility = hiddenObjects.map((object) => object.visible); - this.fogLayers.forEach((layer, index) => { - if (layer.isLowLayer) { - layer.mesh.position.y = THREE.MathUtils.lerp(1.5, 18.0, this.params.fogHeight) + index * 2.8; - layer.mesh.scale.set( - THREE.MathUtils.lerp(0.92, 1.28, this.params.fogRange), - 1, - THREE.MathUtils.lerp(0.92, 1.24, this.params.fogRange) - ); - layer.mesh.material.opacity = layer.baseOpacity * nearSeaMist; - layer.mesh.material.color.copy(horizonColor.clone().lerp(fogColor, 0.72)); - } else { - layer.mesh.position.y = heightBase + (index - 2) * verticalSpread * 0.42; - layer.mesh.scale.setScalar(THREE.MathUtils.lerp(0.82, 1.2, this.params.fogRange)); - layer.mesh.material.opacity = layer.baseOpacity * THREE.MathUtils.lerp(0.16, 1.18, this.params.fogDensity) * rangeOpacity; - layer.mesh.material.color.copy(fogLayerColor); - } - layer.mesh.visible = layer.mesh.material.opacity > 0.01; + hiddenObjects.forEach((object) => { + object.visible = false; }); - if (this.horizonFog) { - this.horizonFog.material.color.copy(horizonColor.clone().lerp(fogColor, 0.18)); - this.horizonFog.material.opacity = - THREE.MathUtils.lerp(0.12, 0.42, this.params.fogDensity) * - THREE.MathUtils.lerp(0.82, 1.34, this.params.fogRange); - this.horizonFog.position.y = THREE.MathUtils.lerp(52, 168, this.params.fogHeight); - this.horizonFog.scale.set( - THREE.MathUtils.lerp(0.92, 1.34, this.params.fogRange), - THREE.MathUtils.lerp(0.72, 1.12, this.params.fogHeight), - THREE.MathUtils.lerp(0.92, 1.34, this.params.fogRange) - ); - this.horizonFog.visible = this.horizonFog.material.opacity > 0.01; - } + const previousRenderTarget = this.renderer.getRenderTarget(); + this.renderer.setRenderTarget(this.depthTarget); + this.renderer.clear(); + this.renderer.render(this.scene, this.camera); + this.renderer.setRenderTarget(previousRenderTarget); - if (this.skyHazeBand) { - this.skyHazeBand.material.color.copy(skyBlendColor); - this.skyHazeBand.material.opacity = - THREE.MathUtils.lerp(0.04, 0.18, this.params.fogDensity) * - THREE.MathUtils.lerp(0.78, 1.08, this.params.fogRange); - const nightHaze = THREE.MathUtils.clamp((-this.params.elevation + 1.0) / 12.0, 0, 1); - this.skyHazeBand.material.color.lerp(new THREE.Color(0x203754), nightHaze * 0.72); - this.skyHazeBand.material.opacity += nightHaze * 0.12; - this.skyHazeBand.position.y = THREE.MathUtils.lerp(340, 560, this.params.fogHeight); - this.skyHazeBand.scale.set( - THREE.MathUtils.lerp(0.98, 1.12, this.params.fogRange), - THREE.MathUtils.lerp(0.92, 1.1, this.params.fogHeight), - THREE.MathUtils.lerp(0.98, 1.12, this.params.fogRange) - ); - this.skyHazeBand.visible = this.skyHazeBand.material.opacity > 0.01; - } + hiddenObjects.forEach((object, index) => { + object.visible = previousVisibility[index]; + }); } animate() { @@ -2183,20 +2314,6 @@ export class OceanScene { this.starField.material.uniforms.time.value = time; } - 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); } @@ -2218,9 +2335,14 @@ export class OceanScene { if (this.snowPass) { this.snowPass.material.uniforms.time.value = time; } + if (this.fogPass) { + this.fogPass.material.uniforms.time.value = time; + } this.controls.update(); + this.updateFog(); if (this.composer) { + this.updateDepthTarget(); this.composer.render(); } else { this.renderer.render(this.scene, this.camera); diff --git a/src/main.js b/src/main.js index 61d85a9..a16058c 100644 --- a/src/main.js +++ b/src/main.js @@ -54,9 +54,8 @@ function setupControls(oceanScene) { 'cloudCoverage', 'cloudDensity', 'cloudElevation', + 'fogEnabled', 'fogDensity', - 'fogHeight', - 'fogRange', 'rainEnabled', 'rainScreenIntensity', 'rainVeilIntensity', @@ -85,9 +84,8 @@ function setupControls(oceanScene) { cloudCoverage: '云层覆盖度', cloudDensity: '云层密度', cloudElevation: '云层高度', + fogEnabled: '是否启用雾气', fogDensity: '雾气浓度', - fogHeight: '雾气高度', - fogRange: '雾气范围', rainEnabled: '是否启用雨效', rainScreenIntensity: '屏幕雨滴强度', rainVeilIntensity: '雨线强度', @@ -202,11 +200,6 @@ function setupControls(oceanScene) { bindController(cloudFolder.add(params, 'cloudDensity', 0, 1, 0.01).name('密度'), (value) => oceanScene.setCloudDensity(value)); bindController(cloudFolder.add(params, 'cloudElevation', 0, 1, 0.01).name('高度'), (value) => oceanScene.setCloudElevation(value)); - const fogFolder = gui.addFolder('雾气'); - bindController(fogFolder.add(params, 'fogDensity', 0, 1, 0.01).name('浓度'), (value) => oceanScene.setFogDensity(value)); - bindController(fogFolder.add(params, 'fogHeight', 0, 1, 0.01).name('高度'), (value) => oceanScene.setFogHeight(value)); - bindController(fogFolder.add(params, 'fogRange', 0, 1, 0.01).name('范围'), (value) => oceanScene.setFogRange(value)); - const rainFolder = gui.addFolder('雨效'); bindController(rainFolder.add(params, 'rainEnabled').name('启用雨效'), (value) => oceanScene.setRainEnabled(value)); bindController(rainFolder.add(params, 'rainVeilIntensity', 0.5, 2.5, 0.01).name('雨线强度'), (value) => oceanScene.setRainVeilIntensity(value)); @@ -214,6 +207,10 @@ function setupControls(oceanScene) { bindController(rainFolder.add(params, 'rainAudioVolume', 0, 1, 0.01).name('雨声音量'), (value) => oceanScene.setRainAudioVolume(value)); bindController(rainFolder.add(params, 'lightningEnabled').name('启用雷闪'), (value) => oceanScene.setLightningEnabled(value)); + const fogFolder = gui.addFolder('雾气'); + bindController(fogFolder.add(params, 'fogEnabled').name('启用雾气'), (value) => oceanScene.setFogEnabled(value)); + bindController(fogFolder.add(params, 'fogDensity', 0, 1, 0.01).name('雾气浓度'), (value) => oceanScene.setFogDensity(value)); + const snowFolder = gui.addFolder('雪效'); bindController(snowFolder.add(params, 'snowEnabled').name('启用降雪'), (value) => oceanScene.setSnowEnabled(value)); bindController(snowFolder.add(params, 'snowIntensity', 0, 1.5, 0.01).name('雪量'), (value) => oceanScene.setSnowIntensity(value)); diff --git a/src/weatherPresets.js b/src/weatherPresets.js index 181e004..78439d6 100644 --- a/src/weatherPresets.js +++ b/src/weatherPresets.js @@ -11,6 +11,7 @@ export const DEFAULT_SCENE_PARAMS = { cloudCoverage: 0.26, cloudDensity: 0.38, cloudElevation: 0.66, + fogEnabled: true, fogDensity: 0.16, fogHeight: 0.26, fogRange: 0.38,