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°
+
+
+
+
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);