diff --git a/session.txt b/session.txt index 4c9384a..02a8660 100644 --- a/session.txt +++ b/session.txt @@ -1 +1 @@ -codex resume 019d2462-a1f1-7a72-947b-70470e482854 +codex resume 019d284e-bc41-78f0-a961-32ce95bfa96a diff --git a/src/OceanScene.js b/src/OceanScene.js index b5b811a..fc80eeb 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -23,13 +23,18 @@ export class OceanScene { this.controls = null; this.water = null; this.sky = null; + this.starField = null; + this.moonSprite = null; + this.galaxyBand = null; this.sun = new THREE.Vector3(); this.terrain = null; this.vegetation = null; this.vegetationSystem = null; this.pmremGenerator = null; this.renderTarget = null; + this.ambientLight = null; this.sunLight = null; + this.moonLight = null; this.vegetationFillLight = null; this.lightningLight = null; this.lightningCloudGlow = null; @@ -74,6 +79,8 @@ export class OceanScene { this.initPostProcessing(); this.initAudio(); await this.initSky(); + this.initStars(); + this.initNightSky(); this.initClouds(); this.initFog(); await this.initWater(); @@ -136,8 +143,8 @@ export class OceanScene { } initLighting() { - const ambientLight = new THREE.AmbientLight(0x555555); - this.scene.add(ambientLight); + this.ambientLight = new THREE.AmbientLight(0x8ea0b7, 0.58); + this.scene.add(this.ambientLight); this.sunLight = new THREE.DirectionalLight(0xffffff, 1.5); this.sunLight.castShadow = true; @@ -151,6 +158,10 @@ export class OceanScene { this.sunLight.shadow.camera.bottom = -100; this.scene.add(this.sunLight); + this.moonLight = new THREE.DirectionalLight(0xa9c7ff, 0); + this.moonLight.castShadow = false; + this.scene.add(this.moonLight); + this.vegetationFillLight = new THREE.DirectionalLight(0xffb06a, 0.95); this.vegetationFillLight.castShadow = false; this.scene.add(this.vegetationFillLight); @@ -532,6 +543,193 @@ export class OceanScene { this.pmremGenerator = new THREE.PMREMGenerator(this.renderer); } + initStars() { + const starCount = 8000; + const positions = new Float32Array(starCount * 3); + const colors = new Float32Array(starCount * 3); + const sizes = new Float32Array(starCount); + const color = new THREE.Color(); + + for (let i = 0; i < starCount; i++) { + const radius = THREE.MathUtils.randFloat(2200, 4200); + const theta = Math.random() * Math.PI * 2.0; + const phi = THREE.MathUtils.randFloat(0.015, Math.PI * 0.49); + const sinPhi = Math.sin(phi); + const x = radius * sinPhi * Math.cos(theta); + const y = radius * Math.cos(phi); + const z = radius * sinPhi * Math.sin(theta); + + positions[i * 3] = x; + positions[i * 3 + 1] = y; + positions[i * 3 + 2] = z; + + color.setHSL( + THREE.MathUtils.randFloat(0.52, 0.64), + THREE.MathUtils.randFloat(0.15, 0.45), + THREE.MathUtils.randFloat(0.72, 0.96) + ); + colors[i * 3] = color.r; + colors[i * 3 + 1] = color.g; + colors[i * 3 + 2] = color.b; + sizes[i] = Math.pow(Math.random(), 1.9); + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + geometry.setAttribute('sizeNoise', new THREE.BufferAttribute(sizes, 1)); + + const material = new THREE.ShaderMaterial({ + uniforms: { + time: { value: 0 }, + intensity: { value: this.params.starIntensity } + }, + vertexShader: ` + attribute float sizeNoise; + varying vec3 vColor; + varying float vPulse; + uniform float time; + uniform float intensity; + + void main() { + vColor = color; + vPulse = fract(sizeNoise * 17.0 + time * (0.015 + sizeNoise * 0.035)); + vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); + gl_Position = projectionMatrix * mvPosition; + float starSize = 2.8 + sizeNoise * 6.4; + float projectedSize = starSize * (6500.0 / -mvPosition.z) * (0.65 + intensity * 0.55); + gl_PointSize = max(1.6, projectedSize); + } + `, + fragmentShader: ` + varying vec3 vColor; + varying float vPulse; + uniform float intensity; + + void main() { + vec2 p = gl_PointCoord - vec2(0.5); + float d = length(p); + float core = smoothstep(0.26, 0.0, d); + float glow = smoothstep(0.58, 0.06, d); + float halo = smoothstep(0.82, 0.16, d); + float twinkle = 0.8 + 0.2 * sin(vPulse * 6.2831); + float alpha = (core * 1.2 + glow * 0.65 + halo * 0.18) * twinkle * intensity; + if (alpha <= 0.001) discard; + gl_FragColor = vec4(vColor, alpha); + } + `, + transparent: true, + depthTest: false, + depthWrite: false, + blending: THREE.AdditiveBlending, + vertexColors: true + }); + + this.starField = new THREE.Points(geometry, material); + this.starField.frustumCulled = false; + this.scene.add(this.starField); + this.updateStars(); + } + + initNightSky() { + const moonTexture = this.createMoonTexture(); + const moonMaterial = new THREE.SpriteMaterial({ + map: moonTexture, + color: 0xe9f0ff, + transparent: true, + depthWrite: false, + depthTest: false, + blending: THREE.AdditiveBlending + }); + this.moonSprite = new THREE.Sprite(moonMaterial); + this.moonSprite.scale.setScalar(980); + this.scene.add(this.moonSprite); + + const galaxyTexture = this.createGalaxyTexture(); + const galaxyMaterial = new THREE.SpriteMaterial({ + map: galaxyTexture, + color: 0xa7bedf, + transparent: true, + opacity: 0, + depthWrite: false, + depthTest: false, + blending: THREE.AdditiveBlending + }); + this.galaxyBand = new THREE.Sprite(galaxyMaterial); + this.galaxyBand.scale.set(2600, 760, 1); + this.scene.add(this.galaxyBand); + } + + createMoonTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + const ctx = canvas.getContext('2d'); + const glow = ctx.createRadialGradient(128, 128, 0, 128, 128, 128); + glow.addColorStop(0, 'rgba(255,255,255,0.98)'); + glow.addColorStop(0.2, 'rgba(244,248,255,0.98)'); + glow.addColorStop(0.34, 'rgba(228,238,255,0.96)'); + glow.addColorStop(0.58, 'rgba(175,205,255,0.42)'); + glow.addColorStop(0.82, 'rgba(126,164,235,0.12)'); + glow.addColorStop(1, 'rgba(120,150,220,0)'); + ctx.fillStyle = glow; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = 'rgba(150,170,210,0.18)'; + ctx.beginPath(); + ctx.arc(152, 104, 34, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.arc(98, 152, 20, 0, Math.PI * 2); + ctx.fill(); + ctx.globalCompositeOperation = 'source-over'; + + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; + } + + createGalaxyTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 1024; + canvas.height = 256; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const base = ctx.createLinearGradient(0, canvas.height * 0.5, canvas.width, canvas.height * 0.5); + base.addColorStop(0, 'rgba(0,0,0,0)'); + base.addColorStop(0.16, 'rgba(110,130,170,0.04)'); + base.addColorStop(0.35, 'rgba(165,180,215,0.1)'); + base.addColorStop(0.5, 'rgba(235,235,255,0.16)'); + base.addColorStop(0.68, 'rgba(165,180,215,0.1)'); + base.addColorStop(0.84, 'rgba(110,130,170,0.04)'); + base.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = base; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + for (let i = 0; i < 1800; i++) { + const x = Math.random() * canvas.width; + const yCenter = canvas.height * 0.52 + Math.sin(x * 0.008) * 22.0; + const y = yCenter + (Math.random() - 0.5) * 120; + const alpha = Math.random() * Math.random() * 0.7; + const size = Math.random() < 0.06 ? 2.2 : 1.0; + ctx.fillStyle = `rgba(255,255,255,${alpha})`; + ctx.fillRect(x, y, size, size); + } + + const blur = ctx.createRadialGradient(canvas.width * 0.5, canvas.height * 0.5, 10, canvas.width * 0.5, canvas.height * 0.5, canvas.width * 0.5); + blur.addColorStop(0, 'rgba(210,220,255,0.09)'); + blur.addColorStop(0.5, 'rgba(150,170,220,0.04)'); + blur.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = blur; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; + } + initClouds() { this.cloudGroup = new THREE.Group(); this.cloudGroup.position.y = 40; @@ -1043,6 +1241,22 @@ export class OceanScene { this.water.rotation.x = -Math.PI / 2; this.water.position.y = -0.15; this.setWaterColor(this.params.waterColor); + + const baseWaterOnBeforeRender = this.water.onBeforeRender.bind(this.water); + this.water.onBeforeRender = (...args) => { + const starsWereVisible = this.starField?.visible ?? false; + if (this.starField) { + this.starField.visible = false; + } + try { + baseWaterOnBeforeRender(...args); + } finally { + if (this.starField) { + this.starField.visible = starsWereVisible; + } + } + }; + this.scene.add(this.water); } @@ -1135,6 +1349,25 @@ export class OceanScene { this.sun.y * sunDistance, this.sun.z * sunDistance ); + const dayMix = THREE.MathUtils.clamp((this.sun.y + 0.06) / 0.52, 0, 1); + this.sunLight.intensity = THREE.MathUtils.lerp(0.0, 1.5, dayMix); + } + + const nightMix = THREE.MathUtils.clamp((-this.sun.y + 0.02) / 0.72, 0, 1); + + if (this.ambientLight) { + this.ambientLight.color.set(0x8ea0b7).lerp(new THREE.Color(0x425a77), nightMix); + this.ambientLight.intensity = THREE.MathUtils.lerp(0.66, 1.0, nightMix); + } + + if (this.moonLight) { + const moonDistance = 115; + this.moonLight.position.set( + -this.sun.x * moonDistance, + Math.max(34, -this.sun.y * moonDistance * 0.5 + 42), + -this.sun.z * moonDistance + ); + this.moonLight.intensity = 1.15 * nightMix; } if (this.vegetationFillLight) { @@ -1144,7 +1377,8 @@ export class OceanScene { Math.max(18, this.sun.y * fillDistance * 0.28 + 24), -this.sun.z * fillDistance * 0.35 + 28 ); - this.vegetationFillLight.intensity = THREE.MathUtils.lerp(0.7, 1.15, THREE.MathUtils.clamp((this.sun.y + 0.2) / 0.9, 0, 1)); + const fillDayMix = THREE.MathUtils.clamp((this.sun.y + 0.2) / 0.9, 0, 1); + this.vegetationFillLight.intensity = THREE.MathUtils.lerp(0.5, 1.15, fillDayMix) + nightMix * 0.16; } if (this.renderTarget) { @@ -1159,13 +1393,16 @@ export class OceanScene { this.updateFog(); this.updateClouds(); + this.updateStars(); } getFogColor() { const elevation = this.params.elevation; - if (elevation < 0) { - return 0x1a2a3a; + if (elevation < -4) { + return 0x121c2c; + } else if (elevation < 0) { + return 0x1d2b40; } else if (elevation < 10) { return 0x4a5a6a; } else if (elevation < 20) { @@ -1182,13 +1419,17 @@ export class OceanScene { getAtmosphereColors() { const sunMix = THREE.MathUtils.clamp((this.params.elevation + 10) / 100, 0, 1); const fogColor = new THREE.Color(this.getFogColor()); + const nightMix = THREE.MathUtils.clamp((-this.params.elevation + 1.0) / 12.0, 0, 1); const warmHorizon = new THREE.Color(0xf0c7a3); const coolHorizon = new THREE.Color(0xcfe0ee); - const horizonColor = warmHorizon.clone().lerp(coolHorizon, sunMix); + const nightHorizon = new THREE.Color(0x27415f); + const horizonColor = warmHorizon.clone().lerp(coolHorizon, sunMix).lerp(nightHorizon, nightMix); const warmSkyBase = new THREE.Color(0xf6d7b8); const coolSkyBase = new THREE.Color(0xbfd8eb); - const skyBaseColor = warmSkyBase.clone().lerp(coolSkyBase, sunMix * 0.92); - const skyBlendColor = skyBaseColor.clone().lerp(fogColor, 0.42); + const nightSkyBase = new THREE.Color(0x08111f); + const nightSkyBlend = new THREE.Color(0x182940); + const skyBaseColor = warmSkyBase.clone().lerp(coolSkyBase, sunMix * 0.92).lerp(nightSkyBase, nightMix); + const skyBlendColor = skyBaseColor.clone().lerp(fogColor, 0.42).lerp(nightSkyBlend, nightMix * 0.78); return { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor }; } @@ -1376,6 +1617,16 @@ export class OceanScene { } } + setStarEnabled(value) { + this.params.starEnabled = value; + this.updateStars(); + } + + setStarIntensity(value) { + this.params.starIntensity = value; + this.updateStars(); + } + updateRainAudioState() { if (this.rainAudioPool.length === 0) return; @@ -1485,6 +1736,8 @@ export class OceanScene { this.setSnowIntensity(mergedParams.snowIntensity); this.setSnowSpeed(mergedParams.snowSpeed); this.setSnowEnabled(mergedParams.snowEnabled); + this.setStarIntensity(mergedParams.starIntensity); + this.setStarEnabled(mergedParams.starEnabled); this.setLightningIntensity(mergedParams.lightningIntensity); this.setLightningEnabled(mergedParams.lightningEnabled); this.setRainEnabled(mergedParams.rainEnabled); @@ -1686,6 +1939,46 @@ export class OceanScene { }); } + updateStars() { + if (!this.starField) return; + + const nightFactor = THREE.MathUtils.clamp((12.0 - this.params.elevation) / 16.0, 0.0, 1.0); + const weatherFade = this.params.rainEnabled ? 0.08 : this.params.snowEnabled ? 0.55 : 1.0; + const opacity = (this.params.starEnabled ? this.params.starIntensity : 0.0) * nightFactor * weatherFade; + + this.starField.visible = opacity > 0.001; + this.starField.material.uniforms.intensity.value = opacity; + + if (this.moonSprite) { + const moonAngle = THREE.MathUtils.degToRad(this.params.azimuth - 42); + const moonDistance = 3200; + this.moonSprite.position.set( + Math.cos(moonAngle) * moonDistance, + THREE.MathUtils.lerp(260, 520, nightFactor), + Math.sin(moonAngle) * moonDistance + ); + const moonGlow = THREE.MathUtils.clamp((nightFactor - 0.18) / 0.72, 0, 1); + this.moonSprite.visible = moonGlow > 0.01; + this.moonSprite.material.opacity = THREE.MathUtils.lerp(0.0, 2.4, moonGlow); + const moonScale = THREE.MathUtils.lerp(820, 1460, moonGlow); + this.moonSprite.scale.setScalar(moonScale); + } + + if (this.galaxyBand) { + const angle = THREE.MathUtils.degToRad(this.params.azimuth + 36); + const radius = 3000; + this.galaxyBand.position.set( + Math.cos(angle) * radius, + THREE.MathUtils.lerp(920, 1500, nightFactor), + Math.sin(angle) * radius + ); + this.galaxyBand.material.rotation = THREE.MathUtils.degToRad(-24); + const galaxyFade = THREE.MathUtils.clamp((opacity - 0.18) / 1.1, 0, 1) * weatherFade; + this.galaxyBand.material.opacity = galaxyFade * 0.92; + this.galaxyBand.visible = this.galaxyBand.material.opacity > 0.01; + } + } + updateFog() { const { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor } = this.getAtmosphereColors(); const fogDensity = THREE.MathUtils.lerp(0.00015, 0.0018, this.params.fogDensity); @@ -1745,6 +2038,9 @@ export class OceanScene { this.skyHazeBand.material.opacity = THREE.MathUtils.lerp(0.04, 0.18, this.params.fogDensity) * THREE.MathUtils.lerp(0.78, 1.08, this.params.fogRange); + const nightHaze = THREE.MathUtils.clamp((-this.params.elevation + 1.0) / 12.0, 0, 1); + this.skyHazeBand.material.color.lerp(new THREE.Color(0x203754), nightHaze * 0.72); + this.skyHazeBand.material.opacity += nightHaze * 0.12; this.skyHazeBand.position.y = THREE.MathUtils.lerp(340, 560, this.params.fogHeight); this.skyHazeBand.scale.set( THREE.MathUtils.lerp(0.98, 1.12, this.params.fogRange), @@ -1774,6 +2070,10 @@ export class OceanScene { }); } + if (this.starField) { + this.starField.material.uniforms.time.value = time; + } + if (this.fogGroup) { this.fogLayers.forEach((layer, index) => { layer.texture.offset.x = time * layer.speedX; @@ -1795,6 +2095,7 @@ export class OceanScene { if (this.lightningFlash > 0.001) { this.updateClouds(); this.updateFog(); + this.updateStars(); } if (this.rainPass) { diff --git a/src/main.js b/src/main.js index 6cf6a4c..e1b75c4 100644 --- a/src/main.js +++ b/src/main.js @@ -68,6 +68,8 @@ function setupControls(oceanScene) { 'snowEnabled', 'snowIntensity', 'snowSpeed', + 'starEnabled', + 'starIntensity', 'lightningEnabled', 'lightningIntensity' ]; @@ -96,6 +98,8 @@ function setupControls(oceanScene) { snowEnabled: '是否启用降雪', snowIntensity: '雪量', snowSpeed: '降雪速度', + starEnabled: '是否启用星空', + starIntensity: '星空强度', lightningEnabled: '是否启用雷闪', lightningIntensity: '雷闪强度' }; @@ -135,12 +139,25 @@ function setupControls(oceanScene) { }; const refreshControllers = () => { controllers.forEach((controller) => controller.updateDisplay()); + updateStarControllerState(); + }; + const setControllerEnabled = (controller, enabled) => { + controller.domElement.style.opacity = enabled ? '1' : '0.45'; + controller.domElement.style.pointerEvents = enabled ? 'auto' : 'none'; + controller.enable?.(); + if (!enabled) { + controller.disable?.(); + } + controller.domElement.querySelectorAll('input, select, button').forEach((element) => { + element.disabled = !enabled; + }); }; const bindController = (controller, applyValue) => { controllers.push(controller); controller.onChange((value) => { applyValue(value); markPresetCustom(); + updateStarControllerState(); }); return controller; }; @@ -154,11 +171,23 @@ function setupControls(oceanScene) { gui.add(exportActions, '导出预设'); const skyFolder = gui.addFolder('天空'); - bindController(skyFolder.add(params, 'elevation', 0, 90, 0.1).name('太阳高度'), (value) => oceanScene.setSunElevation(value)); + bindController(skyFolder.add(params, 'elevation', -12, 90, 0.1).name('太阳高度'), (value) => oceanScene.setSunElevation(value)); bindController(skyFolder.add(params, 'azimuth', -180, 180, 0.1).name('太阳方位'), (value) => oceanScene.setSunAzimuth(value)); bindController(skyFolder.add(params, 'exposure', 0, 1, 0.01).name('曝光度'), (value) => oceanScene.setExposure(value)); bindController(skyFolder.add(params, 'turbidity', 1, 20, 0.1).name('浑浊度'), (value) => oceanScene.setTurbidity(value)); bindController(skyFolder.add(params, 'rayleigh', 0, 4, 0.01).name('瑞利散射'), (value) => oceanScene.setRayleigh(value)); + const starEnabledController = bindController(skyFolder.add(params, 'starEnabled').name('启用星空'), (value) => oceanScene.setStarEnabled(value)); + const starIntensityController = bindController(skyFolder.add(params, 'starIntensity', 0, 1.5, 0.01).name('星空强度'), (value) => oceanScene.setStarIntensity(value)); + const updateStarControllerState = () => { + const canUseStars = params.elevation < -1.0; + if (!canUseStars && params.starEnabled) { + oceanScene.setStarEnabled(false); + } + setControllerEnabled(starEnabledController, canUseStars); + setControllerEnabled(starIntensityController, canUseStars); + starEnabledController.updateDisplay(); + starIntensityController.updateDisplay(); + }; const bloomFolder = gui.addFolder('泛光'); bindController(bloomFolder.add(params, 'bloomStrength', 0, 1, 0.01).name('强度'), (value) => oceanScene.setBloomStrength(value)); @@ -194,6 +223,7 @@ function setupControls(oceanScene) { bindController(snowFolder.add(params, 'snowSpeed', 0.2, 2.2, 0.01).name('速度'), (value) => oceanScene.setSnowSpeed(value)); gui.close(); + updateStarControllerState(); } main().catch(console.error); diff --git a/src/weatherPresets.js b/src/weatherPresets.js index b5d4484..04c129a 100644 --- a/src/weatherPresets.js +++ b/src/weatherPresets.js @@ -24,6 +24,8 @@ export const DEFAULT_SCENE_PARAMS = { snowEnabled: false, snowIntensity: 0.65, snowSpeed: 0.85, + starEnabled: true, + starIntensity: 0.7, lightningEnabled: false, lightningIntensity: 0.75, mieCoefficient: 0.005, @@ -55,6 +57,7 @@ export const WEATHER_PRESETS = { fogDensity: 0.12, fogHeight: 0.58, fogRange: 0.28, + starIntensity: 0.22, lightningEnabled: false } }, @@ -76,6 +79,32 @@ export const WEATHER_PRESETS = { fogDensity: 0.1, fogHeight: 0.2, fogRange: 0.24, + starIntensity: 0.0, + lightningEnabled: false + } + }, + night: { + label: '黑夜', + params: { + ...DEFAULT_SCENE_PARAMS, + elevation: -8, + azimuth: 205, + exposure: 0.2, + turbidity: 1.8, + rayleigh: 0.2, + bloomStrength: 0.12, + bloomRadius: 0.08, + waterColor: '#07131f', + cloudCoverage: 0.03, + cloudDensity: 0.08, + cloudElevation: 0.78, + fogDensity: 0.08, + fogHeight: 0.22, + fogRange: 0.2, + rainEnabled: false, + snowEnabled: false, + starEnabled: true, + starIntensity: 0.4, lightningEnabled: false } }, @@ -97,6 +126,7 @@ export const WEATHER_PRESETS = { fogDensity: 0.38, fogHeight: 0.36, fogRange: 0.62, + starIntensity: 0.04, lightningEnabled: false } }, @@ -125,6 +155,7 @@ export const WEATHER_PRESETS = { rainSpeed: 1.16, rainAudioEnabled: true, rainAudioVolume: 0.38, + starIntensity: 0.02, lightningEnabled: true, lightningIntensity: 0.68, snowEnabled: false @@ -148,6 +179,7 @@ export const WEATHER_PRESETS = { fogDensity: 0.56, fogHeight: 0.42, fogRange: 0.74, + starIntensity: 0.16, rainEnabled: false, lightningEnabled: false, snowEnabled: true,