From c5f19afa4b295cc881e3afb181fd73eda1cda130 Mon Sep 17 00:00:00 2001 From: como Date: Wed, 25 Mar 2026 18:30:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E4=BA=91=E5=92=8C=E8=BE=89?= =?UTF-8?q?=E5=85=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 36 ++++++++- src/OceanScene.js | 197 +++++++++++++++++++++++++++++++++++++++++++++- src/main.js | 63 ++++++--------- 3 files changed, 250 insertions(+), 46 deletions(-) diff --git a/index.html b/index.html index 836a14f..6dfada1 100644 --- a/index.html +++ b/index.html @@ -171,16 +171,16 @@
-

☀️ 太阳控制

+

☀️ 场景控制

-
15.0°
+
15.0°
-
180.0°
+
180.0°
@@ -197,6 +197,36 @@
2.00
+
+ + +
0.72
+
+
+ + +
0.28
+
+
+ + +
0.82
+
+
+ + +
0.78
+
+
+ + +
0.72
+
+
+ + +
0.28
+
FPS: 60
diff --git a/src/OceanScene.js b/src/OceanScene.js index cf04540..f4a1dad 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -2,6 +2,9 @@ import * as THREE from 'three'; import { Water } from 'three/addons/objects/Water.js'; import { Sky } from 'three/addons/objects/Sky.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { TerrainGenerator } from './TerrainGenerator.js'; import { VegetationSystem } from './VegetationSystem.js'; @@ -20,13 +23,24 @@ export class OceanScene { this.pmremGenerator = null; this.renderTarget = null; this.sunLight = null; + this.composer = null; + this.bloomPass = null; + this.cloudGroup = null; + this.cloudMaterials = []; + this.cloudLayers = []; this.params = { - elevation: 2, + elevation: 15, azimuth: 180, exposure: 0.5, turbidity: 10, rayleigh: 2, + bloomStrength: 0.72, + bloomRadius: 0.28, + bloomThreshold: 0.82, + cloudOpacity: 0.78, + cloudCoverage: 0.72, + cloudSpeed: 0.28, mieCoefficient: 0.005, mieDirectionalG: 0.8 }; @@ -42,7 +56,9 @@ export class OceanScene { this.initCamera(); this.initControls(); this.initLighting(); + this.initPostProcessing(); await this.initSky(); + this.initClouds(); await this.initWater(); await this.initTerrain(); await this.initVegetation(); @@ -106,6 +122,19 @@ export class OceanScene { this.sunLight.shadow.camera.bottom = -100; this.scene.add(this.sunLight); } + + initPostProcessing() { + this.composer = new EffectComposer(this.renderer); + this.composer.addPass(new RenderPass(this.scene, this.camera)); + + this.bloomPass = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + this.params.bloomStrength, + this.params.bloomRadius, + this.params.bloomThreshold + ); + this.composer.addPass(this.bloomPass); + } async initSky() { this.sky = new Sky(); @@ -120,6 +149,103 @@ export class OceanScene { this.pmremGenerator = new THREE.PMREMGenerator(this.renderer); } + + initClouds() { + const cloudTexture = this.createCloudTexture(); + this.cloudGroup = new THREE.Group(); + this.cloudGroup.position.y = -120; + this.addCloudDomeLayer(cloudTexture, { + radius: 4200, + opacity: 0.42, + repeatX: 5.5, + repeatY: 2.4, + speedX: 0.00045, + speedY: 0.00012 + }); + this.addCloudDomeLayer(cloudTexture, { + radius: 3900, + opacity: 0.28, + repeatX: 7.5, + repeatY: 3.2, + speedX: -0.00032, + speedY: 0.00018 + }); + + this.scene.add(this.cloudGroup); + this.updateClouds(); + } + + addCloudDomeLayer(texture, config) { + const layerTexture = texture.clone(); + layerTexture.wrapS = THREE.RepeatWrapping; + layerTexture.wrapT = THREE.RepeatWrapping; + layerTexture.repeat.set(config.repeatX, config.repeatY); + layerTexture.needsUpdate = true; + + const material = new THREE.MeshBasicMaterial({ + map: layerTexture, + alphaMap: layerTexture, + color: 0xffffff, + transparent: true, + opacity: config.opacity, + side: THREE.BackSide, + depthWrite: false, + alphaTest: 0.08, + fog: false + }); + + const geometry = new THREE.SphereGeometry( + config.radius, + 32, + 20, + 0, + Math.PI * 2, + 0, + Math.PI * 0.48 + ); + + const mesh = new THREE.Mesh(geometry, material); + mesh.rotation.y = Math.random() * Math.PI * 2; + this.cloudMaterials.push(material); + this.cloudLayers.push({ + mesh, + texture: layerTexture, + speedX: config.speedX, + speedY: config.speedY, + baseOpacity: config.opacity + }); + this.cloudGroup.add(mesh); + } + + createCloudTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + + const context = canvas.getContext('2d'); + context.clearRect(0, 0, canvas.width, canvas.height); + + for (let i = 0; i < 28; i++) { + const x = 28 + Math.random() * 200; + const y = 52 + Math.random() * 152; + const radius = 28 + Math.random() * 44; + const gradient = context.createRadialGradient(x, y, 0, x, y, radius); + + gradient.addColorStop(0, 'rgba(255,255,255,0.92)'); + gradient.addColorStop(0.28, 'rgba(255,255,255,0.78)'); + gradient.addColorStop(0.62, 'rgba(255,255,255,0.22)'); + 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; + } async initWater() { const waterGeometry = new THREE.PlaneGeometry(10000, 10000, 128, 128); @@ -210,6 +336,7 @@ export class OceanScene { this.scene.add(this.sky); this.scene.fog.color.setHex(this.getFogColor()); + this.updateClouds(); } getFogColor() { @@ -238,6 +365,9 @@ export class OceanScene { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); + if (this.composer) { + this.composer.setSize(window.innerWidth, window.innerHeight); + } } setSunElevation(value) { @@ -266,6 +396,57 @@ export class OceanScene { this.sky.material.uniforms['rayleigh'].value = value; this.updateSun(); } + + setBloomStrength(value) { + this.params.bloomStrength = value; + if (this.bloomPass) { + this.bloomPass.strength = value; + } + } + + setBloomRadius(value) { + this.params.bloomRadius = value; + if (this.bloomPass) { + this.bloomPass.radius = value; + } + } + + setBloomThreshold(value) { + this.params.bloomThreshold = value; + if (this.bloomPass) { + this.bloomPass.threshold = value; + } + } + + setCloudOpacity(value) { + this.params.cloudOpacity = value; + this.updateClouds(); + } + + setCloudCoverage(value) { + this.params.cloudCoverage = value; + this.updateClouds(); + } + + setCloudSpeed(value) { + this.params.cloudSpeed = value; + } + + updateClouds() { + if (!this.cloudGroup) return; + + const sunMix = THREE.MathUtils.clamp((this.params.elevation + 10) / 100, 0, 1); + const warmCloud = new THREE.Color(0xdab188); + const dayCloud = new THREE.Color(0xd1dbe6); + const cloudColor = warmCloud.lerp(dayCloud, sunMix); + + for (const layer of this.cloudLayers) { + const opacity = layer.baseOpacity * this.params.cloudOpacity * (0.2 + this.params.cloudCoverage * 1.2); + layer.mesh.material.opacity = opacity; + layer.mesh.material.color.copy(cloudColor); + layer.mesh.visible = opacity > 0.015; + } + } animate() { requestAnimationFrame(() => this.animate()); @@ -275,9 +456,21 @@ export class OceanScene { if (this.water) { this.water.material.uniforms['time'].value += 1.0 / 60.0; } + + if (this.cloudGroup) { + this.cloudGroup.rotation.y = time * 0.015 * this.params.cloudSpeed; + this.cloudLayers.forEach((layer) => { + layer.texture.offset.x = time * layer.speedX * this.params.cloudSpeed; + layer.texture.offset.y = time * layer.speedY * this.params.cloudSpeed; + }); + } this.controls.update(); - this.renderer.render(this.scene, this.camera); + if (this.composer) { + this.composer.render(); + } else { + this.renderer.render(this.scene, this.camera); + } this.frameCount++; const currentTime = performance.now(); diff --git a/src/main.js b/src/main.js index 3cb9c88..7552556 100644 --- a/src/main.js +++ b/src/main.js @@ -30,47 +30,28 @@ async function main() { } function setupControls(oceanScene) { - const elevationSlider = document.getElementById('sun-elevation'); - const azimuthSlider = document.getElementById('sun-azimuth'); - const exposureSlider = document.getElementById('exposure'); - const turbiditySlider = document.getElementById('turbidity'); - const rayleighSlider = document.getElementById('rayleigh'); - - const elevationValue = document.getElementById('elevation-value'); - const azimuthValue = document.getElementById('azimuth-value'); - const exposureValue = document.getElementById('exposure-value'); - const turbidityValue = document.getElementById('turbidity-value'); - const rayleighValue = document.getElementById('rayleigh-value'); - - elevationSlider.addEventListener('input', (e) => { - const value = parseFloat(e.target.value); - oceanScene.setSunElevation(value); - elevationValue.textContent = value.toFixed(1) + '°'; - }); - - azimuthSlider.addEventListener('input', (e) => { - const value = parseFloat(e.target.value); - oceanScene.setSunAzimuth(value); - azimuthValue.textContent = value.toFixed(1) + '°'; - }); - - exposureSlider.addEventListener('input', (e) => { - const value = parseFloat(e.target.value); - oceanScene.setExposure(value); - exposureValue.textContent = value.toFixed(2); - }); - - turbiditySlider.addEventListener('input', (e) => { - const value = parseFloat(e.target.value); - oceanScene.setTurbidity(value); - turbidityValue.textContent = value.toFixed(1); - }); - - rayleighSlider.addEventListener('input', (e) => { - const value = parseFloat(e.target.value); - oceanScene.setRayleigh(value); - rayleighValue.textContent = value.toFixed(2); - }); + const bindSlider = (id, formatter, setter) => { + const slider = document.getElementById(id); + const valueLabel = document.getElementById(`${id}-value`); + + slider.addEventListener('input', (e) => { + const value = parseFloat(e.target.value); + setter(value); + valueLabel.textContent = formatter(value); + }); + }; + + bindSlider('sun-elevation', (value) => `${value.toFixed(1)}°`, (value) => oceanScene.setSunElevation(value)); + bindSlider('sun-azimuth', (value) => `${value.toFixed(1)}°`, (value) => oceanScene.setSunAzimuth(value)); + bindSlider('exposure', (value) => value.toFixed(2), (value) => oceanScene.setExposure(value)); + bindSlider('turbidity', (value) => value.toFixed(1), (value) => oceanScene.setTurbidity(value)); + bindSlider('rayleigh', (value) => value.toFixed(2), (value) => oceanScene.setRayleigh(value)); + bindSlider('bloom-strength', (value) => value.toFixed(2), (value) => oceanScene.setBloomStrength(value)); + bindSlider('bloom-radius', (value) => value.toFixed(2), (value) => oceanScene.setBloomRadius(value)); + bindSlider('bloom-threshold', (value) => value.toFixed(2), (value) => oceanScene.setBloomThreshold(value)); + bindSlider('cloud-opacity', (value) => value.toFixed(2), (value) => oceanScene.setCloudOpacity(value)); + bindSlider('cloud-coverage', (value) => value.toFixed(2), (value) => oceanScene.setCloudCoverage(value)); + bindSlider('cloud-speed', (value) => value.toFixed(2), (value) => oceanScene.setCloudSpeed(value)); } main().catch(console.error);