diff --git a/index.html b/index.html
index b75b627..f15e7e6 100644
--- a/index.html
+++ b/index.html
@@ -336,6 +336,21 @@
+
+
FPS: 60
diff --git a/src/OceanScene.js b/src/OceanScene.js
index 79b6838..aa6faba 100644
--- a/src/OceanScene.js
+++ b/src/OceanScene.js
@@ -26,6 +26,7 @@ export class OceanScene {
this.renderTarget = null;
this.sunLight = null;
this.vegetationFillLight = null;
+ this.lightningLight = null;
this.composer = null;
this.bloomPass = null;
this.rainPass = null;
@@ -57,6 +58,8 @@ export class OceanScene {
rainVeilIntensity: 1.15,
rainDropSize: 1.0,
rainSpeed: 1.0,
+ lightningEnabled: false,
+ lightningIntensity: 0.75,
mieCoefficient: 0.005,
mieDirectionalG: 0.8
};
@@ -64,6 +67,9 @@ export class OceanScene {
this.clock = new THREE.Clock();
this.frameCount = 0;
this.lastTime = performance.now();
+ this.lightningFlash = 0;
+ this.lightningBurstEnd = 0;
+ this.nextLightningAt = 0;
}
async init() {
@@ -142,6 +148,11 @@ export class OceanScene {
this.vegetationFillLight = new THREE.DirectionalLight(0xffb06a, 0.95);
this.vegetationFillLight.castShadow = false;
this.scene.add(this.vegetationFillLight);
+
+ this.lightningLight = new THREE.DirectionalLight(0xddeeff, 0);
+ this.lightningLight.castShadow = false;
+ this.lightningLight.position.set(-120, 180, 40);
+ this.scene.add(this.lightningLight);
}
initPostProcessing() {
@@ -1002,6 +1013,74 @@ export class OceanScene {
}
}
+ setLightningEnabled(value) {
+ this.params.lightningEnabled = value;
+ if (!value) {
+ this.lightningFlash = 0;
+ this.lightningBurstEnd = 0;
+ this.nextLightningAt = 0;
+ this.applyLightningState(0);
+ }
+ }
+
+ setLightningIntensity(value) {
+ this.params.lightningIntensity = value;
+ }
+
+ scheduleNextLightning(time) {
+ const rainActivity = Math.max(this.params.rainVeilIntensity, this.params.rainScreenIntensity);
+ const densityBias = THREE.MathUtils.clamp(rainActivity / 1.5, 0, 1);
+ const delay = THREE.MathUtils.lerp(7.5, 3.0, densityBias) + Math.random() * THREE.MathUtils.lerp(8.0, 4.0, densityBias);
+ this.nextLightningAt = time + delay;
+ }
+
+ updateLightning(time) {
+ if (!this.params.lightningEnabled) return;
+
+ if (this.nextLightningAt === 0) {
+ this.scheduleNextLightning(time);
+ }
+
+ if (time >= this.nextLightningAt && time >= this.lightningBurstEnd) {
+ const burstLength = 0.18 + Math.random() * 0.16;
+ this.lightningBurstEnd = time + burstLength;
+ this.lightningLight.position.set(
+ THREE.MathUtils.randFloat(-220, 220),
+ THREE.MathUtils.randFloat(140, 260),
+ THREE.MathUtils.randFloat(-120, 120)
+ );
+ this.scheduleNextLightning(time);
+ }
+
+ if (time < this.lightningBurstEnd) {
+ const burstProgress = 1.0 - (this.lightningBurstEnd - time) / Math.max(this.lightningBurstEnd - (this.lightningBurstEnd - 0.34), 0.0001);
+ const pulseA = Math.max(0, Math.sin((time + 0.13) * 37.0));
+ const pulseB = Math.max(0, Math.sin((time + 0.04) * 83.0));
+ const envelope = Math.exp(-burstProgress * 5.5);
+ const flash = (pulseA * 0.75 + pulseB * 0.45 + 0.25) * envelope * this.params.lightningIntensity;
+ this.lightningFlash = Math.max(this.lightningFlash * 0.72, flash);
+ } else {
+ this.lightningFlash *= 0.82;
+ if (this.lightningFlash < 0.002) this.lightningFlash = 0;
+ }
+
+ this.applyLightningState(this.lightningFlash);
+ }
+
+ applyLightningState(flash) {
+ if (this.lightningLight) {
+ this.lightningLight.intensity = flash * 5.5;
+ }
+
+ if (this.renderer) {
+ this.renderer.toneMappingExposure = this.params.exposure * (1.0 + flash * 1.6);
+ }
+
+ if (this.bloomPass) {
+ this.bloomPass.strength = this.params.bloomStrength + flash * 0.35;
+ }
+ }
+
updateClouds() {
if (!this.cloudGroup) return;
@@ -1009,6 +1088,8 @@ export class OceanScene {
const warmCloud = new THREE.Color(0xdab188);
const dayCloud = new THREE.Color(0xd1dbe6);
const cloudColor = warmCloud.lerp(dayCloud, sunMix);
+ const lightningMix = THREE.MathUtils.clamp(this.lightningFlash * 0.85, 0, 1);
+ cloudColor.lerp(new THREE.Color(0xe9f3ff), lightningMix);
for (const layer of this.cloudLayers) {
const coverageFactor = 0.15 + this.params.cloudCoverage * 1.15;
@@ -1023,6 +1104,10 @@ export class OceanScene {
updateFog() {
const { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor } = this.getAtmosphereColors();
const fogDensity = THREE.MathUtils.lerp(0.00015, 0.0018, this.params.fogDensity);
+ const lightningMix = THREE.MathUtils.clamp(this.lightningFlash * 0.75, 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);
@@ -1077,6 +1162,7 @@ export class OceanScene {
requestAnimationFrame(() => this.animate());
const time = this.clock.getElapsedTime();
+ this.updateLightning(time);
if (this.water) {
this.water.material.uniforms['time'].value += 1.0 / 60.0;
@@ -1108,6 +1194,11 @@ export class OceanScene {
this.vegetationSystem.update(time);
}
+ if (this.lightningFlash > 0.001) {
+ this.updateClouds();
+ this.updateFog();
+ }
+
if (this.rainPass) {
this.rainPass.material.uniforms.time.value = time;
}
diff --git a/src/main.js b/src/main.js
index 7ab6508..f19c042 100644
--- a/src/main.js
+++ b/src/main.js
@@ -68,7 +68,9 @@ function setupControls(oceanScene) {
bindSlider('rain-veil-intensity', (value) => value.toFixed(2), (value) => oceanScene.setRainVeilIntensity(value));
bindSlider('rain-drop-size', (value) => value.toFixed(2), (value) => oceanScene.setRainDropSize(value));
bindSlider('rain-speed', (value) => value.toFixed(2), (value) => oceanScene.setRainSpeed(value));
+ bindSlider('lightning-intensity', (value) => value.toFixed(2), (value) => oceanScene.setLightningIntensity(value));
bindCheckbox('rain-enabled', (value) => oceanScene.setRainEnabled(value));
+ bindCheckbox('lightning-enabled', (value) => oceanScene.setLightningEnabled(value));
}
main().catch(console.error);