diff --git a/index.html b/index.html index 1a75e26..9ff7f6f 100644 --- a/index.html +++ b/index.html @@ -89,36 +89,60 @@ top: 20px; right: 20px; background: rgba(0, 0, 0, 0.7); - padding: 20px; + padding: 15px; border-radius: 10px; color: white; z-index: 100; backdrop-filter: blur(10px); - min-width: 250px; + min-width: 280px; + max-height: calc(100vh - 40px); + overflow-y: auto; } #sun-controls h3 { - margin-bottom: 15px; - font-size: 16px; + margin-bottom: 12px; + font-size: 14px; border-bottom: 1px solid rgba(255,255,255,0.2); - padding-bottom: 10px; + padding-bottom: 8px; + } + + .control-section { + margin-bottom: 10px; + } + + .control-section-title { + font-size: 11px; + opacity: 0.6; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .control-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 12px; } .control-group { - margin-bottom: 15px; + margin-bottom: 0; + } + + .control-group.full-width { + grid-column: 1 / -1; } .control-group label { display: block; - margin-bottom: 5px; - font-size: 13px; - opacity: 0.9; + margin-bottom: 3px; + font-size: 11px; + opacity: 0.85; } .control-group input[type="range"] { width: 100%; - height: 6px; - border-radius: 3px; + height: 4px; + border-radius: 2px; background: rgba(255,255,255,0.2); outline: none; -webkit-appearance: none; @@ -126,8 +150,8 @@ .control-group input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; - width: 16px; - height: 16px; + width: 12px; + height: 12px; border-radius: 50%; background: #4a9eff; cursor: pointer; @@ -135,9 +159,9 @@ .control-value { text-align: right; - font-size: 12px; - opacity: 0.7; - margin-top: 3px; + font-size: 10px; + opacity: 0.6; + margin-top: 1px; } #stats { @@ -172,11 +196,112 @@

☀️ 场景控制

-
- - -
2.0°
+ +
+
太阳 & 光照
+
+
+ + +
2.0°
+
+
+ + +
180.0°
+
+
+ + +
0.10
+
+
+ + +
10.0
+
+
+ + +
2.00
+
+
+ +
+
Bloom 效果
+
+
+ + +
0.10
+
+
+ + +
0.00
+
+
+
+ +
+
云层
+
+
+ + +
0.40
+
+
+ + +
0.50
+
+
+ + +
0.50
+
+
+
+ +
+
雾气
+
+
+ + +
0.42
+
+
+ + +
0.32
+
+
+ + +
0.55
+
+
+
+ +
+
海天线柔和
+
+
+ + +
0.45
+
+
+ + +
0.28
+
+
+
+
diff --git a/src/OceanScene.js b/src/OceanScene.js index 670a535..e599f2f 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -34,6 +34,8 @@ export class OceanScene { this.fogLayers = []; this.horizonFog = null; this.skyHazeBand = null; + this.horizonSoftBlend = null; + this.horizonGlow = null; this.params = { elevation: 2, @@ -51,7 +53,9 @@ export class OceanScene { fogHeight: 0.32, fogRange: 0.55, mieCoefficient: 0.005, - mieDirectionalG: 0.8 + mieDirectionalG: 0.8, + horizonBlend: 0.45, + horizonGlow: 0.28 }; this.clock = new THREE.Clock(); @@ -285,6 +289,10 @@ export class OceanScene { this.fogGroup.add(this.horizonFog); this.skyHazeBand = this.createSkyHazeBand(); this.fogGroup.add(this.skyHazeBand); + this.horizonSoftBlend = this.createHorizonSoftBlend(); + this.fogGroup.add(this.horizonSoftBlend); + this.horizonGlow = this.createHorizonGlow(); + this.fogGroup.add(this.horizonGlow); this.scene.add(this.fogGroup); this.updateFog(); } @@ -357,10 +365,13 @@ export class OceanScene { const context = canvas.getContext('2d'); const gradient = context.createLinearGradient(0, 0, 0, canvas.height); gradient.addColorStop(0, 'rgba(255,255,255,0)'); - gradient.addColorStop(0.16, 'rgba(255,255,255,0.08)'); - gradient.addColorStop(0.38, 'rgba(255,255,255,0.62)'); - gradient.addColorStop(0.6, 'rgba(255,255,255,0.42)'); - gradient.addColorStop(0.82, 'rgba(255,255,255,0.06)'); + gradient.addColorStop(0.08, 'rgba(255,255,255,0.02)'); + gradient.addColorStop(0.2, 'rgba(255,255,255,0.12)'); + gradient.addColorStop(0.35, 'rgba(255,255,255,0.38)'); + gradient.addColorStop(0.5, 'rgba(255,255,255,0.55)'); + gradient.addColorStop(0.65, 'rgba(255,255,255,0.38)'); + gradient.addColorStop(0.8, 'rgba(255,255,255,0.12)'); + gradient.addColorStop(0.92, 'rgba(255,255,255,0.02)'); gradient.addColorStop(1, 'rgba(255,255,255,0)'); context.fillStyle = gradient; context.fillRect(0, 0, canvas.width, canvas.height); @@ -382,10 +393,14 @@ export class OceanScene { const context = canvas.getContext('2d'); const gradient = context.createLinearGradient(0, 0, 0, canvas.height); gradient.addColorStop(0, 'rgba(255,255,255,0)'); - gradient.addColorStop(0.12, 'rgba(255,255,255,0.04)'); - gradient.addColorStop(0.34, 'rgba(255,255,255,0.32)'); - gradient.addColorStop(0.56, 'rgba(255,255,255,0.78)'); - gradient.addColorStop(0.82, 'rgba(255,255,255,0.18)'); + gradient.addColorStop(0.06, 'rgba(255,255,255,0.01)'); + gradient.addColorStop(0.15, 'rgba(255,255,255,0.06)'); + gradient.addColorStop(0.28, 'rgba(255,255,255,0.18)'); + gradient.addColorStop(0.42, 'rgba(255,255,255,0.45)'); + gradient.addColorStop(0.55, 'rgba(255,255,255,0.62)'); + gradient.addColorStop(0.7, 'rgba(255,255,255,0.45)'); + gradient.addColorStop(0.85, 'rgba(255,255,255,0.12)'); + gradient.addColorStop(0.94, 'rgba(255,255,255,0.02)'); gradient.addColorStop(1, 'rgba(255,255,255,0)'); context.fillStyle = gradient; context.fillRect(0, 0, canvas.width, canvas.height); @@ -441,6 +456,105 @@ export class OceanScene { mesh.userData.texture = texture; return mesh; } + + createHorizonSoftBlendTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 64; + canvas.height = 512; + + const context = canvas.getContext('2d'); + const gradient = context.createLinearGradient(0, 0, 0, canvas.height); + gradient.addColorStop(0, 'rgba(255,255,255,0)'); + gradient.addColorStop(0.15, 'rgba(255,255,255,0.01)'); + gradient.addColorStop(0.3, 'rgba(255,255,255,0.08)'); + gradient.addColorStop(0.45, 'rgba(255,255,255,0.25)'); + gradient.addColorStop(0.5, 'rgba(255,255,255,0.32)'); + gradient.addColorStop(0.55, 'rgba(255,255,255,0.25)'); + gradient.addColorStop(0.7, 'rgba(255,255,255,0.08)'); + gradient.addColorStop(0.85, 'rgba(255,255,255,0.01)'); + gradient.addColorStop(1, 'rgba(255,255,255,0)'); + context.fillStyle = gradient; + context.fillRect(0, 0, canvas.width, canvas.height); + + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.ClampToEdgeWrapping; + texture.repeat.set(5, 1); + texture.needsUpdate = true; + return texture; + } + + createHorizonSoftBlend() { + const texture = this.createHorizonSoftBlendTexture(); + const material = new THREE.MeshBasicMaterial({ + map: texture, + alphaMap: texture, + color: 0xe8f0f5, + transparent: true, + opacity: 0.45, + fog: false, + depthWrite: false, + side: THREE.BackSide, + blending: THREE.NormalBlending + }); + + const geometry = new THREE.CylinderGeometry(4850, 4850, 1200, 72, 1, true); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.y = 180; + mesh.renderOrder = -3; + mesh.userData.texture = texture; + return mesh; + } + + createHorizonGlowTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 64; + canvas.height = 512; + + const context = canvas.getContext('2d'); + const gradient = context.createLinearGradient(0, 0, 0, canvas.height); + gradient.addColorStop(0, 'rgba(255,255,255,0)'); + gradient.addColorStop(0.25, 'rgba(255,255,255,0.02)'); + gradient.addColorStop(0.4, 'rgba(255,255,255,0.12)'); + gradient.addColorStop(0.48, 'rgba(255,255,255,0.22)'); + gradient.addColorStop(0.52, 'rgba(255,255,255,0.22)'); + gradient.addColorStop(0.6, 'rgba(255,255,255,0.12)'); + gradient.addColorStop(0.75, 'rgba(255,255,255,0.02)'); + gradient.addColorStop(1, 'rgba(255,255,255,0)'); + context.fillStyle = gradient; + context.fillRect(0, 0, canvas.width, canvas.height); + + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.ClampToEdgeWrapping; + texture.repeat.set(6, 1); + texture.needsUpdate = true; + return texture; + } + + createHorizonGlow() { + const texture = this.createHorizonGlowTexture(); + const material = new THREE.MeshBasicMaterial({ + map: texture, + alphaMap: texture, + color: 0xfff5e8, + transparent: true, + opacity: 0.28, + fog: false, + depthWrite: false, + side: THREE.BackSide, + blending: THREE.AdditiveBlending + }); + + const geometry = new THREE.CylinderGeometry(4600, 4600, 600, 72, 1, true); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.y = 120; + mesh.renderOrder = -4; + mesh.userData.texture = texture; + return mesh; + } async initWater() { const waterGeometry = new THREE.PlaneGeometry(10000, 10000, 128, 128); @@ -711,6 +825,16 @@ export class OceanScene { this.updateFog(); } + setHorizonBlend(value) { + this.params.horizonBlend = value; + this.updateFog(); + } + + setHorizonGlow(value) { + this.params.horizonGlow = value; + this.updateFog(); + } + updateClouds() { if (!this.cloudGroup) return; @@ -780,6 +904,41 @@ export class OceanScene { ); this.skyHazeBand.visible = this.skyHazeBand.material.opacity > 0.01; } + + if (this.horizonSoftBlend) { + const softBlendColor = horizonColor.clone().lerp(skyBlendColor, 0.35); + this.horizonSoftBlend.material.color.copy(softBlendColor); + this.horizonSoftBlend.material.opacity = + this.params.horizonBlend * + THREE.MathUtils.lerp(0.8, 1.2, this.params.fogDensity) * + THREE.MathUtils.lerp(0.85, 1.1, this.params.fogRange); + this.horizonSoftBlend.position.y = THREE.MathUtils.lerp(120, 220, this.params.fogHeight); + this.horizonSoftBlend.scale.set( + THREE.MathUtils.lerp(0.9, 1.18, this.params.fogRange), + THREE.MathUtils.lerp(0.85, 1.12, this.params.fogHeight), + THREE.MathUtils.lerp(0.9, 1.18, this.params.fogRange) + ); + this.horizonSoftBlend.visible = this.horizonSoftBlend.material.opacity > 0.01; + } + + if (this.horizonGlow) { + const warmGlow = new THREE.Color(0xfff5e8); + const coolGlow = new THREE.Color(0xe8f4ff); + const glowColor = warmGlow.clone().lerp(coolGlow, sunMix); + this.horizonGlow.material.color.copy(glowColor); + this.horizonGlow.material.opacity = + this.params.horizonGlow * + THREE.MathUtils.lerp(0.7, 1.1, this.params.fogDensity) * + THREE.MathUtils.lerp(0.8, 1.05, this.params.fogRange) * + THREE.MathUtils.clamp((this.params.elevation + 5) / 50, 0.1, 1); + this.horizonGlow.position.y = THREE.MathUtils.lerp(80, 160, this.params.fogHeight); + this.horizonGlow.scale.set( + THREE.MathUtils.lerp(0.92, 1.15, this.params.fogRange), + THREE.MathUtils.lerp(0.88, 1.1, this.params.fogHeight), + THREE.MathUtils.lerp(0.92, 1.15, this.params.fogRange) + ); + this.horizonGlow.visible = this.horizonGlow.material.opacity > 0.01; + } } animate() { @@ -811,6 +970,12 @@ export class OceanScene { if (this.skyHazeBand?.userData.texture) { this.skyHazeBand.userData.texture.offset.x = -time * 0.00018; } + if (this.horizonSoftBlend?.userData.texture) { + this.horizonSoftBlend.userData.texture.offset.x = time * 0.00012; + } + if (this.horizonGlow?.userData.texture) { + this.horizonGlow.userData.texture.offset.x = -time * 0.00008; + } } if (this.vegetationSystem) { diff --git a/src/main.js b/src/main.js index 9c48ea3..db25070 100644 --- a/src/main.js +++ b/src/main.js @@ -55,6 +55,8 @@ function setupControls(oceanScene) { bindSlider('fog-density', (value) => value.toFixed(2), (value) => oceanScene.setFogDensity(value)); bindSlider('fog-height', (value) => value.toFixed(2), (value) => oceanScene.setFogHeight(value)); bindSlider('fog-range', (value) => value.toFixed(2), (value) => oceanScene.setFogRange(value)); + bindSlider('horizon-blend', (value) => value.toFixed(2), (value) => oceanScene.setHorizonBlend(value)); + bindSlider('horizon-glow', (value) => value.toFixed(2), (value) => oceanScene.setHorizonGlow(value)); } main().catch(console.error);