diff --git a/src/OceanScene.js b/src/OceanScene.js index aa6faba..1b6c7df 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -27,6 +27,7 @@ export class OceanScene { this.sunLight = null; this.vegetationFillLight = null; this.lightningLight = null; + this.lightningCloudGlow = null; this.composer = null; this.bloomPass = null; this.rainPass = null; @@ -68,8 +69,10 @@ export class OceanScene { this.frameCount = 0; this.lastTime = performance.now(); this.lightningFlash = 0; + this.lightningLocalFlash = 0; this.lightningBurstEnd = 0; this.nextLightningAt = 0; + this.lightningPulseSchedule = []; } async init() { @@ -443,11 +446,32 @@ export class OceanScene { }); this.scene.add(this.cloudGroup); + this.initLightningCloudGlow(); this.setCloudElevation(this.params.cloudElevation); this.setCloudCoverage(this.params.cloudCoverage); this.updateClouds(); } + initLightningCloudGlow() { + const glowTexture = this.createLightningGlowTexture(); + const material = new THREE.SpriteMaterial({ + map: glowTexture, + color: 0xddeeff, + transparent: true, + opacity: 0, + depthWrite: false, + depthTest: false, + fog: false, + blending: THREE.AdditiveBlending + }); + + this.lightningCloudGlow = new THREE.Sprite(material); + this.lightningCloudGlow.scale.set(900, 900, 1); + this.lightningCloudGlow.position.set(-420, 210, -760); + this.lightningCloudGlow.visible = false; + this.scene.add(this.lightningCloudGlow); + } + addCloudDomeLayer(texture, config) { const layerTexture = texture.clone(); layerTexture.wrapS = THREE.RepeatWrapping; @@ -576,6 +600,27 @@ export class OceanScene { return texture; } + createLightningGlowTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + + const context = canvas.getContext('2d'); + const gradient = context.createRadialGradient(128, 128, 0, 128, 128, 128); + gradient.addColorStop(0, 'rgba(255,255,255,0.95)'); + gradient.addColorStop(0.18, 'rgba(220,236,255,0.85)'); + gradient.addColorStop(0.42, 'rgba(180,205,255,0.35)'); + gradient.addColorStop(0.72, 'rgba(120,155,255,0.08)'); + gradient.addColorStop(1, 'rgba(120,155,255,0)'); + context.fillStyle = gradient; + context.fillRect(0, 0, canvas.width, canvas.height); + + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + texture.needsUpdate = true; + return texture; + } + createFogTexture() { const canvas = document.createElement('canvas'); canvas.width = 512; @@ -1034,6 +1079,37 @@ export class OceanScene { this.nextLightningAt = time + delay; } + startLightningBurst(time) { + const pulseCountRoll = Math.random(); + const pulseCount = pulseCountRoll > 0.8 ? 3 : pulseCountRoll > 0.45 ? 2 : 1; + this.lightningPulseSchedule = []; + + let pulseTime = time; + for (let i = 0; i < pulseCount; i++) { + const duration = 0.05 + Math.random() * 0.07; + const amplitude = this.params.lightningIntensity * (1.0 - i * 0.14) * (0.85 + Math.random() * 0.35); + this.lightningPulseSchedule.push({ + time: pulseTime, + duration, + amplitude + }); + pulseTime += 0.06 + Math.random() * 0.12; + } + + this.lightningBurstEnd = pulseTime + 0.12; + + const flashX = THREE.MathUtils.randFloat(-1600, 1600); + const flashY = THREE.MathUtils.randFloat(120, 340); + const flashZ = THREE.MathUtils.randFloat(-1800, -420); + this.lightningLight.position.set(flashX * 0.22, flashY, flashZ * 0.08); + + if (this.lightningCloudGlow) { + this.lightningCloudGlow.position.set(flashX, flashY, flashZ); + const size = THREE.MathUtils.randFloat(720, 1480); + this.lightningCloudGlow.scale.set(size, size * THREE.MathUtils.randFloat(0.72, 1.08), 1); + } + } + updateLightning(time) { if (!this.params.lightningEnabled) return; @@ -1042,36 +1118,46 @@ export class OceanScene { } 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.startLightningBurst(time); 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); + let flash = 0; + let localFlash = 0; + for (const pulse of this.lightningPulseSchedule) { + const dt = time - pulse.time; + if (dt < -0.001 || dt > pulse.duration * 2.8) continue; + + const attack = Math.exp(-Math.pow((dt - pulse.duration * 0.12) / Math.max(pulse.duration * 0.32, 0.001), 2.0)); + const decay = Math.exp(-Math.max(dt, 0.0) / Math.max(pulse.duration * 1.45, 0.001)); + const pulseFlash = pulse.amplitude * attack * decay; + flash += pulseFlash; + localFlash += pulseFlash * 1.35; + } + + if (flash > 0.001) { + this.lightningFlash = Math.max(this.lightningFlash * 0.7, flash); + this.lightningLocalFlash = Math.max(this.lightningLocalFlash * 0.62, localFlash); } else { this.lightningFlash *= 0.82; if (this.lightningFlash < 0.002) this.lightningFlash = 0; + this.lightningLocalFlash *= 0.74; + if (this.lightningLocalFlash < 0.002) this.lightningLocalFlash = 0; } - this.applyLightningState(this.lightningFlash); + this.applyLightningState(this.lightningFlash, this.lightningLocalFlash); } - applyLightningState(flash) { + applyLightningState(flash, localFlash = 0) { if (this.lightningLight) { this.lightningLight.intensity = flash * 5.5; } + if (this.lightningCloudGlow) { + this.lightningCloudGlow.visible = localFlash > 0.002; + this.lightningCloudGlow.material.opacity = THREE.MathUtils.clamp(localFlash * 0.95, 0, 0.95); + } + if (this.renderer) { this.renderer.toneMappingExposure = this.params.exposure * (1.0 + flash * 1.6); } @@ -1088,7 +1174,7 @@ 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); + const lightningMix = THREE.MathUtils.clamp(this.lightningFlash * 0.32, 0, 1); cloudColor.lerp(new THREE.Color(0xe9f3ff), lightningMix); for (const layer of this.cloudLayers) { @@ -1104,7 +1190,7 @@ 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); + 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);