From 9b4dfb68b03853364f2d2f99fae3772a5ab47195 Mon Sep 17 00:00:00 2001 From: como Date: Thu, 26 Mar 2026 13:31:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E9=9B=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 20 ++++++++ src/OceanScene.js | 117 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.js | 3 ++ 3 files changed, 140 insertions(+) diff --git a/index.html b/index.html index aa9d6f4..db58eb6 100644 --- a/index.html +++ b/index.html @@ -348,6 +348,26 @@ +
+
雪效
+
+ + +
+
+
+ + +
0.65
+
+
+ + +
0.85
+
+
+
+
雷闪
diff --git a/src/OceanScene.js b/src/OceanScene.js index 34c14a3..6541fc5 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -34,6 +34,7 @@ export class OceanScene { this.composer = null; this.bloomPass = null; this.rainPass = null; + this.snowPass = null; this.cloudGroup = null; this.cloudMaterials = []; this.cloudLayers = []; @@ -72,6 +73,9 @@ export class OceanScene { rainSpeed: 1.0, rainAudioEnabled: true, rainAudioVolume: 0.35, + snowEnabled: false, + snowIntensity: 0.65, + snowSpeed: 0.85, lightningEnabled: true, lightningIntensity: 0.75, mieCoefficient: 0.005, @@ -192,6 +196,13 @@ export class OceanScene { this.rainPass.material.uniforms.dropSize.value = this.params.rainDropSize; this.rainPass.material.uniforms.rainSpeed.value = this.params.rainSpeed; this.composer.addPass(this.rainPass); + + this.snowPass = new ShaderPass(this.createSnowShader()); + this.snowPass.enabled = this.params.snowEnabled; + this.snowPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); + this.snowPass.material.uniforms.intensity.value = this.params.snowIntensity; + this.snowPass.material.uniforms.snowSpeed.value = this.params.snowSpeed; + this.composer.addPass(this.snowPass); } initAudio() { @@ -441,6 +452,85 @@ export class OceanScene { ` }; } + + createSnowShader() { + return { + uniforms: { + tDiffuse: { value: null }, + time: { value: 0 }, + intensity: { value: this.params.snowIntensity }, + snowSpeed: { value: this.params.snowSpeed }, + resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) } + }, + vertexShader: ` + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D tDiffuse; + uniform float time; + uniform float intensity; + uniform float snowSpeed; + uniform vec2 resolution; + + varying vec2 vUv; + + float rnd(float x) { + return fract(sin(dot(vec2(x + 47.49, 38.2467 / (x + 2.3)), vec2(12.9898, 78.233))) * 43758.5453); + } + + float drawCircle(vec2 uv, vec2 center, float radius) { + return 1.0 - smoothstep(0.0, radius, length(uv - center)); + } + + float snowField(vec2 uv, float t, float amount, float aspect) { + float snow = 0.0; + float blizzardFactor = mix(0.08, 0.3, amount); + int flakeCount = 220; + + for (int i = 0; i < 220; i++) { + if (i >= flakeCount) break; + float j = float(i); + float baseRnd = rnd(cos(j)); + float speed = (0.3 + baseRnd * (0.7 + 0.5 * cos(j / 55.0))) * mix(0.7, 1.65, amount); + float radius = (0.001 + speed * 0.012) * mix(0.7, 1.18, amount); + vec2 center = vec2( + ((0.25 - uv.y) * blizzardFactor + rnd(j) + 0.08 * cos(t * 0.7 + sin(j))) * aspect, + mod(sin(j) - speed * (t * 1.5 * (0.1 + blizzardFactor)), 1.35) - 0.25 + ); + + float flake = drawCircle(uv, center, radius); + snow += flake * (0.035 + speed * 0.04); + } + + return snow; + } + + void main() { + vec2 uv = vUv; + float aspect = resolution.x / max(resolution.y, 1.0); + vec2 snowUv = vec2(vUv.x * aspect, vUv.y); + float snowAmount = clamp(intensity / 1.5, 0.0, 1.0); + float t = time * snowSpeed; + float snow = snowField(snowUv, t, snowAmount, aspect); + float snowMask = clamp(snow * mix(0.45, 1.15, snowAmount), 0.0, 1.0); + float atmosphere = (1.0 - vUv.y) * 0.12 * snowAmount; + + vec3 base = texture2D(tDiffuse, vUv).rgb; + vec3 snowTint = vec3(0.92, 0.95, 1.0); + base = mix(base, base * 0.96 + snowTint * 0.04, snowAmount * 0.1); + base += snowTint * snowMask; + base += vec3(0.16, 0.28, 0.4) * atmosphere; + + gl_FragColor = vec4(base, 1.0); + } + ` + }; + } async initSky() { this.sky = new Sky(); @@ -973,6 +1063,9 @@ export class OceanScene { if (this.rainPass) { this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); } + if (this.snowPass) { + this.snowPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); + } } setSunElevation(value) { @@ -1111,6 +1204,27 @@ export class OceanScene { this.updateRainAudioState(); } + setSnowEnabled(value) { + this.params.snowEnabled = value; + if (this.snowPass) { + this.snowPass.enabled = value; + } + } + + setSnowIntensity(value) { + this.params.snowIntensity = value; + if (this.snowPass) { + this.snowPass.material.uniforms.intensity.value = value; + } + } + + setSnowSpeed(value) { + this.params.snowSpeed = value; + if (this.snowPass) { + this.snowPass.material.uniforms.snowSpeed.value = value; + } + } + updateRainAudioState() { if (this.rainAudioPool.length === 0) return; @@ -1465,6 +1579,9 @@ export class OceanScene { if (this.rainPass) { this.rainPass.material.uniforms.time.value = time; } + if (this.snowPass) { + this.snowPass.material.uniforms.time.value = time; + } this.controls.update(); if (this.composer) { diff --git a/src/main.js b/src/main.js index 121cb94..efc94c0 100644 --- a/src/main.js +++ b/src/main.js @@ -69,9 +69,12 @@ function setupControls(oceanScene) { bindSlider('rain-drop-size', (value) => value.toFixed(2), (value) => oceanScene.setRainDropSize(value)); bindSlider('rain-speed', (value) => value.toFixed(2), (value) => oceanScene.setRainSpeed(value)); bindSlider('rain-audio-volume', (value) => value.toFixed(2), (value) => oceanScene.setRainAudioVolume(value)); + bindSlider('snow-intensity', (value) => value.toFixed(2), (value) => oceanScene.setSnowIntensity(value)); + bindSlider('snow-speed', (value) => value.toFixed(2), (value) => oceanScene.setSnowSpeed(value)); bindSlider('lightning-intensity', (value) => value.toFixed(2), (value) => oceanScene.setLightningIntensity(value)); bindCheckbox('rain-enabled', (value) => oceanScene.setRainEnabled(value)); bindCheckbox('rain-audio-enabled', (value) => oceanScene.setRainAudioEnabled(value)); + bindCheckbox('snow-enabled', (value) => oceanScene.setSnowEnabled(value)); bindCheckbox('lightning-enabled', (value) => oceanScene.setLightningEnabled(value)); }