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°
+
+
+
+
+
+
+
+
+
+
+
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);