加入云和辉光

This commit is contained in:
2026-03-25 18:30:39 +08:00
parent f8d3192deb
commit c5f19afa4b
3 changed files with 250 additions and 46 deletions

View File

@@ -171,16 +171,16 @@
</div> </div>
<div id="sun-controls"> <div id="sun-controls">
<h3>☀️ 太阳控制</h3> <h3>☀️ 场景控制</h3>
<div class="control-group"> <div class="control-group">
<label>太阳高度角 (Elevation)</label> <label>太阳高度角 (Elevation)</label>
<input type="range" id="sun-elevation" min="-10" max="90" value="15" step="0.1"> <input type="range" id="sun-elevation" min="-10" max="90" value="15" step="0.1">
<div class="control-value" id="elevation-value">15.0°</div> <div class="control-value" id="sun-elevation-value">15.0°</div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label>太阳方位角 (Azimuth)</label> <label>太阳方位角 (Azimuth)</label>
<input type="range" id="sun-azimuth" min="-180" max="180" value="180" step="0.1"> <input type="range" id="sun-azimuth" min="-180" max="180" value="180" step="0.1">
<div class="control-value" id="azimuth-value">180.0°</div> <div class="control-value" id="sun-azimuth-value">180.0°</div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label>曝光度 (Exposure)</label> <label>曝光度 (Exposure)</label>
@@ -197,6 +197,36 @@
<input type="range" id="rayleigh" min="0" max="4" value="2" step="0.01"> <input type="range" id="rayleigh" min="0" max="4" value="2" step="0.01">
<div class="control-value" id="rayleigh-value">2.00</div> <div class="control-value" id="rayleigh-value">2.00</div>
</div> </div>
<div class="control-group">
<label>Bloom 强度 (Bloom Strength)</label>
<input type="range" id="bloom-strength" min="0" max="2.5" value="0.72" step="0.01">
<div class="control-value" id="bloom-strength-value">0.72</div>
</div>
<div class="control-group">
<label>Bloom 扩散 (Bloom Radius)</label>
<input type="range" id="bloom-radius" min="0" max="1" value="0.28" step="0.01">
<div class="control-value" id="bloom-radius-value">0.28</div>
</div>
<div class="control-group">
<label>Bloom 阈值 (Bloom Threshold)</label>
<input type="range" id="bloom-threshold" min="0" max="1.5" value="0.82" step="0.01">
<div class="control-value" id="bloom-threshold-value">0.82</div>
</div>
<div class="control-group">
<label>云层透明度 (Cloud Opacity)</label>
<input type="range" id="cloud-opacity" min="0" max="1" value="0.78" step="0.01">
<div class="control-value" id="cloud-opacity-value">0.78</div>
</div>
<div class="control-group">
<label>云层覆盖度 (Cloud Coverage)</label>
<input type="range" id="cloud-coverage" min="0" max="1" value="0.72" step="0.01">
<div class="control-value" id="cloud-coverage-value">0.72</div>
</div>
<div class="control-group">
<label>云层速度 (Cloud Speed)</label>
<input type="range" id="cloud-speed" min="0" max="2" value="0.28" step="0.01">
<div class="control-value" id="cloud-speed-value">0.28</div>
</div>
</div> </div>
<div id="stats">FPS: <span id="fps">60</span></div> <div id="stats">FPS: <span id="fps">60</span></div>

View File

@@ -2,6 +2,9 @@ import * as THREE from 'three';
import { Water } from 'three/addons/objects/Water.js'; import { Water } from 'three/addons/objects/Water.js';
import { Sky } from 'three/addons/objects/Sky.js'; import { Sky } from 'three/addons/objects/Sky.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.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 { TerrainGenerator } from './TerrainGenerator.js';
import { VegetationSystem } from './VegetationSystem.js'; import { VegetationSystem } from './VegetationSystem.js';
@@ -20,13 +23,24 @@ export class OceanScene {
this.pmremGenerator = null; this.pmremGenerator = null;
this.renderTarget = null; this.renderTarget = null;
this.sunLight = null; this.sunLight = null;
this.composer = null;
this.bloomPass = null;
this.cloudGroup = null;
this.cloudMaterials = [];
this.cloudLayers = [];
this.params = { this.params = {
elevation: 2, elevation: 15,
azimuth: 180, azimuth: 180,
exposure: 0.5, exposure: 0.5,
turbidity: 10, turbidity: 10,
rayleigh: 2, rayleigh: 2,
bloomStrength: 0.72,
bloomRadius: 0.28,
bloomThreshold: 0.82,
cloudOpacity: 0.78,
cloudCoverage: 0.72,
cloudSpeed: 0.28,
mieCoefficient: 0.005, mieCoefficient: 0.005,
mieDirectionalG: 0.8 mieDirectionalG: 0.8
}; };
@@ -42,7 +56,9 @@ export class OceanScene {
this.initCamera(); this.initCamera();
this.initControls(); this.initControls();
this.initLighting(); this.initLighting();
this.initPostProcessing();
await this.initSky(); await this.initSky();
this.initClouds();
await this.initWater(); await this.initWater();
await this.initTerrain(); await this.initTerrain();
await this.initVegetation(); await this.initVegetation();
@@ -107,6 +123,19 @@ export class OceanScene {
this.scene.add(this.sunLight); 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() { async initSky() {
this.sky = new Sky(); this.sky = new Sky();
this.sky.scale.setScalar(10000); this.sky.scale.setScalar(10000);
@@ -121,6 +150,103 @@ export class OceanScene {
this.pmremGenerator = new THREE.PMREMGenerator(this.renderer); 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() { async initWater() {
const waterGeometry = new THREE.PlaneGeometry(10000, 10000, 128, 128); const waterGeometry = new THREE.PlaneGeometry(10000, 10000, 128, 128);
@@ -210,6 +336,7 @@ export class OceanScene {
this.scene.add(this.sky); this.scene.add(this.sky);
this.scene.fog.color.setHex(this.getFogColor()); this.scene.fog.color.setHex(this.getFogColor());
this.updateClouds();
} }
getFogColor() { getFogColor() {
@@ -238,6 +365,9 @@ export class OceanScene {
this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setSize(window.innerWidth, window.innerHeight);
if (this.composer) {
this.composer.setSize(window.innerWidth, window.innerHeight);
}
} }
setSunElevation(value) { setSunElevation(value) {
@@ -267,6 +397,57 @@ export class OceanScene {
this.updateSun(); 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() { animate() {
requestAnimationFrame(() => this.animate()); requestAnimationFrame(() => this.animate());
@@ -276,8 +457,20 @@ export class OceanScene {
this.water.material.uniforms['time'].value += 1.0 / 60.0; 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.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++; this.frameCount++;
const currentTime = performance.now(); const currentTime = performance.now();

View File

@@ -30,47 +30,28 @@ async function main() {
} }
function setupControls(oceanScene) { function setupControls(oceanScene) {
const elevationSlider = document.getElementById('sun-elevation'); const bindSlider = (id, formatter, setter) => {
const azimuthSlider = document.getElementById('sun-azimuth'); const slider = document.getElementById(id);
const exposureSlider = document.getElementById('exposure'); const valueLabel = document.getElementById(`${id}-value`);
const turbiditySlider = document.getElementById('turbidity');
const rayleighSlider = document.getElementById('rayleigh');
const elevationValue = document.getElementById('elevation-value'); slider.addEventListener('input', (e) => {
const azimuthValue = document.getElementById('azimuth-value'); const value = parseFloat(e.target.value);
const exposureValue = document.getElementById('exposure-value'); setter(value);
const turbidityValue = document.getElementById('turbidity-value'); valueLabel.textContent = formatter(value);
const rayleighValue = document.getElementById('rayleigh-value'); });
};
elevationSlider.addEventListener('input', (e) => { bindSlider('sun-elevation', (value) => `${value.toFixed(1)}°`, (value) => oceanScene.setSunElevation(value));
const value = parseFloat(e.target.value); bindSlider('sun-azimuth', (value) => `${value.toFixed(1)}°`, (value) => oceanScene.setSunAzimuth(value));
oceanScene.setSunElevation(value); bindSlider('exposure', (value) => value.toFixed(2), (value) => oceanScene.setExposure(value));
elevationValue.textContent = value.toFixed(1) + '°'; 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));
azimuthSlider.addEventListener('input', (e) => { bindSlider('bloom-radius', (value) => value.toFixed(2), (value) => oceanScene.setBloomRadius(value));
const value = parseFloat(e.target.value); bindSlider('bloom-threshold', (value) => value.toFixed(2), (value) => oceanScene.setBloomThreshold(value));
oceanScene.setSunAzimuth(value); bindSlider('cloud-opacity', (value) => value.toFixed(2), (value) => oceanScene.setCloudOpacity(value));
azimuthValue.textContent = value.toFixed(1) + '°'; bindSlider('cloud-coverage', (value) => value.toFixed(2), (value) => oceanScene.setCloudCoverage(value));
}); bindSlider('cloud-speed', (value) => value.toFixed(2), (value) => oceanScene.setCloudSpeed(value));
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);
});
} }
main().catch(console.error); main().catch(console.error);