From eecd6cd0bd6374dfc1a84fe35547b1eff69cd88d Mon Sep 17 00:00:00 2001 From: como Date: Thu, 26 Mar 2026 12:47:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E9=9B=A8=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 51 ++++++++ src/OceanScene.js | 288 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.js | 14 +++ 3 files changed, 353 insertions(+) diff --git a/index.html b/index.html index 3c759ff..b75b627 100644 --- a/index.html +++ b/index.html @@ -156,6 +156,27 @@ background: #4a9eff; cursor: pointer; } + + .control-toggle { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 10px; + border-radius: 8px; + background: rgba(255,255,255,0.06); + margin-bottom: 8px; + } + + .control-toggle label { + font-size: 12px; + opacity: 0.9; + } + + .control-toggle input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: #5da9ff; + } .control-value { text-align: right; @@ -285,6 +306,36 @@ + +
+
雨效
+
+ + +
+
+
+ + +
0.41
+
+
+ + +
1.15
+
+
+ + +
1.00
+
+
+ + +
1.00
+
+
+
FPS: 60
diff --git a/src/OceanScene.js b/src/OceanScene.js index 58518aa..79b6838 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -5,6 +5,7 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; +import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; import { TerrainGenerator } from './TerrainGenerator.js'; import { VegetationSystem } from './VegetationSystem.js'; @@ -27,6 +28,7 @@ export class OceanScene { this.vegetationFillLight = null; this.composer = null; this.bloomPass = null; + this.rainPass = null; this.cloudGroup = null; this.cloudMaterials = []; this.cloudLayers = []; @@ -50,6 +52,11 @@ export class OceanScene { fogDensity: 0.42, fogHeight: 0.32, fogRange: 0.55, + rainEnabled: false, + rainScreenIntensity: 0.41, + rainVeilIntensity: 1.15, + rainDropSize: 1.0, + rainSpeed: 1.0, mieCoefficient: 0.005, mieDirectionalG: 0.8 }; @@ -148,6 +155,245 @@ export class OceanScene { this.params.bloomThreshold ); this.composer.addPass(this.bloomPass); + + this.rainPass = new ShaderPass(this.createRainShader()); + this.rainPass.enabled = this.params.rainEnabled; + this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); + this.rainPass.material.uniforms.screenIntensity.value = this.params.rainScreenIntensity; + this.rainPass.material.uniforms.veilIntensity.value = this.params.rainVeilIntensity; + this.rainPass.material.uniforms.dropSize.value = this.params.rainDropSize; + this.rainPass.material.uniforms.rainSpeed.value = this.params.rainSpeed; + this.composer.addPass(this.rainPass); + } + + createRainShader() { + return { + uniforms: { + tDiffuse: { value: null }, + time: { value: 0 }, + screenIntensity: { value: this.params.rainScreenIntensity }, + veilIntensity: { value: this.params.rainVeilIntensity }, + dropSize: { value: this.params.rainDropSize }, + rainSpeed: { value: this.params.rainSpeed }, + 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 screenIntensity; + uniform float veilIntensity; + uniform float dropSize; + uniform float rainSpeed; + uniform vec2 resolution; + + varying vec2 vUv; + + float hash11(float p) { + p = fract(p * 0.1031); + p *= p + 33.33; + p *= p + p; + return fract(p); + } + + float hash21(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); + } + + vec3 hash31(float p) { + vec3 p3 = fract(vec3(p) * vec3(0.1031, 0.11369, 0.13787)); + p3 += dot(p3, p3.yzx + 19.19); + return fract(vec3( + (p3.x + p3.y) * p3.z, + (p3.x + p3.z) * p3.y, + (p3.y + p3.z) * p3.x + )); + } + + float easeOutCubic(float x) { + float inv = 1.0 - clamp(x, 0.0, 1.0); + return 1.0 - inv * inv * inv; + } + + vec2 dropLayer(vec2 uv, float t, float sizeFactor) { + vec2 grid = vec2(9.0, 4.0) / mix(0.7, 1.45, sizeFactor); + vec2 p = uv * grid; + vec2 cell = floor(p); + vec2 local = fract(p) - 0.5; + vec3 n = hash31(cell.x * 53.17 + cell.y * 417.43); + + float rate = (0.45 + n.z * 0.95) * mix(0.82, 1.45, sizeFactor); + float cycle = t * rate + n.z * 11.0; + float phase = fract(cycle); + + vec2 spawn = (n.xy - 0.5) * vec2(0.72, 0.28); + float onset = smoothstep(0.0, 0.07, phase); + float lifeFade = 1.0 - smoothstep(0.52, 0.82, phase); + float fall = easeOutCubic(smoothstep(0.02, 0.58, phase)); + float fallDistance = mix(0.9, 2.15, n.y) * mix(0.85, 1.4, sizeFactor) * fall; + vec2 center = vec2(spawn.x, 0.62 + spawn.y - fallDistance); + + vec2 delta = local - center; + float aspect = mix(0.8, 1.45, n.x); + float bodyRadius = mix(0.18, 0.3, sizeFactor); + float body = smoothstep(bodyRadius, 0.03, length(delta * vec2(aspect, 1.0))); + + float tail = smoothstep(mix(0.06, 0.095, sizeFactor), 0.0, abs(delta.x)) * + smoothstep(0.04, -0.12, delta.y) * + smoothstep(-0.72, -0.04, delta.y) * + smoothstep(0.04, 0.18, phase); + + float spitSeed = hash21(cell + floor(cycle) + 3.7); + float spitY = center.y + 0.18 + fract(local.y * 3.7 + spitSeed) * 0.42; + float spit = smoothstep(mix(0.06, 0.11, sizeFactor), 0.0, length(local - vec2(center.x + (spitSeed - 0.5) * 0.14, spitY))); + spit *= smoothstep(0.02, 0.14, phase) * (1.0 - smoothstep(0.18, 0.34, phase)); + + float impactFlash = smoothstep(0.14, 0.0, phase) * smoothstep(0.24, 0.02, length(local - vec2(spawn.x, 0.58 + spawn.y))); + float mask = (max(body, tail * 0.9) * onset + spit * 0.55 + impactFlash * 0.7) * lifeFade; + float trail = tail * onset * lifeFade; + return vec2(mask, trail); + } + + vec2 drops(vec2 uv, float t, float l0, float l1, float l2, float sizeFactor) { + vec2 m1 = dropLayer(uv, t, sizeFactor) * l1; + vec2 m2 = dropLayer(uv * 1.85, t, sizeFactor * 0.82) * l2; + + float c = m1.x + m2.x; + c = smoothstep(0.3, 1.0, c); + + float trail = max(m1.y * l1, m2.y * l2); + return vec2(c, trail); + } + + vec3 impactLayer(vec2 uv, float t, float rainAmount, float sizeFactor) { + vec2 grid = vec2(11.0, 6.0) / mix(0.75, 1.35, sizeFactor); + vec2 p = uv * grid; + vec2 cell = floor(p); + vec2 local = fract(p) - 0.5; + vec3 n = hash31(cell.x * 97.13 + cell.y * 413.71); + + float rate = (1.3 + n.z * 2.4) * mix(0.95, 1.55, sizeFactor); + float cycle = t * rate + n.x * 7.13 + n.y * 3.17; + float phase = fract(cycle); + vec2 center = (n.xy - 0.5) * vec2(0.72, 0.36); + float lifeFade = 1.0 - smoothstep(0.28, 0.56, phase); + center.y += easeOutCubic(smoothstep(0.01, 0.36, phase)) * mix(0.9, 1.45, sizeFactor); + vec2 d = local - center; + float radius = mix(0.012, mix(0.18, 0.32, sizeFactor), smoothstep(0.0, 0.22, phase)); + + float flash = smoothstep(0.09, 0.0, phase) * smoothstep(0.18, 0.015, length(d)); + float ring = smoothstep(0.025, 0.0, abs(length(d) - radius)) * (1.0 - smoothstep(0.05, 0.24, phase)); + float tail = smoothstep(0.055, 0.0, abs(d.x)) * + smoothstep(0.02, -0.12, d.y) * + smoothstep(-0.6, -0.04, d.y) * + smoothstep(0.02, 0.18, phase); + float sideSplash = smoothstep(0.055, 0.0, abs(d.y + 0.02)) * + smoothstep(0.16, 0.02, abs(abs(d.x) - mix(0.06, 0.22, phase))) * + (1.0 - smoothstep(0.06, 0.2, phase)); + + float mask = (flash + ring * 0.55 + tail * 0.65 + sideSplash * 0.4) * step(0.64, n.z) * rainAmount * lifeFade; + vec2 dir = normalize(d + vec2(0.0001)); + return vec3(mask, dir * mask * (0.018 + flash * 0.028)); + } + + float rainVeil(vec2 uv, float t, float rainAmount) { + float accum = 0.0; + for (int i = 0; i < 4; i++) { + float fi = float(i); + float scale = 18.0 + fi * 11.0; + float speed = 2.0 + fi * 0.9; + float slant = 0.16 + fi * 0.07; + vec2 p = uv; + p.x += p.y * slant; + p.y += t * speed; + p.x += sin(p.y * (7.0 + fi * 2.3) + fi * 11.7) * (0.015 + fi * 0.004); + + vec2 layerUv = p * scale; + vec2 cell = floor(layerUv); + vec2 local = fract(layerUv) - 0.5; + vec3 n = hash31(cell.x * (41.0 + fi * 13.0) + cell.y * (289.0 + fi * 71.0) + fi * 19.0); + + float lane = (n.x - 0.5) * 0.62; + float width = mix(0.01, 0.045, n.y * n.y); + float length = mix(0.24, 0.95, n.x); + float bend = sin((cell.y + t * speed * 8.0) * (0.9 + n.z * 1.6) + n.x * 6.2831) * 0.18; + float dx = local.x - lane - bend * (0.15 + fi * 0.05); + float dy = local.y + (n.z - 0.5) * 0.3; + + float streak = smoothstep(width, width * 0.15, abs(dx)); + streak *= smoothstep(length, length * 0.12, abs(dy) * 2.0); + + float breakup = smoothstep(0.2, 0.85, sin((dy + n.y * 2.0) * 18.0 + n.z * 12.0) * 0.5 + 0.5); + float flicker = 0.35 + 0.65 * fract(n.z * 31.7 + t * (4.0 + n.x * 3.0) + fi * 2.1); + float density = step(0.34 - fi * 0.05, n.y); + streak *= mix(1.0, breakup, 0.55) * flicker * density; + + accum += streak * (0.52 - fi * 0.08); + } + + return accum * smoothstep(0.08, 0.95, rainAmount); + } + + void main() { + vec2 uv = vUv; + vec2 centeredUv = (uv - 0.5) * vec2(resolution.x / max(resolution.y, 1.0), 1.0); + float screenAmount = clamp(screenIntensity / 1.5, 0.0, 1.0); + float veilAmount = clamp(veilIntensity / 1.5, 0.0, 1.0); + float sizeFactor = clamp((dropSize - 0.4) / 1.4, 0.0, 1.0); + float rainAmount = max(screenAmount, veilAmount); + float t = time * rainSpeed * 0.18; + + float staticLayer = 0.0; + float densityScale = mix(1.2, 0.55, sizeFactor); + float layer1 = smoothstep(0.08, 0.55, screenAmount) * densityScale; + float layer2 = smoothstep(0.25, 0.95, screenAmount) * densityScale; + + vec2 c = drops(centeredUv * 0.9, t, staticLayer, layer1, layer2, sizeFactor); + vec3 impact = impactLayer(centeredUv * vec2(1.0, 1.2), t * 2.2, screenAmount, sizeFactor); + float veil = rainVeil(centeredUv * vec2(1.0, 1.4), time * rainSpeed * 0.55, veilAmount); + vec2 e = vec2(0.0015, 0.0); + float cx = drops(centeredUv + e, t, staticLayer, layer1, layer2, sizeFactor).x; + float cy = drops(centeredUv + e.yx, t, staticLayer, layer1, layer2, sizeFactor).x; + vec2 normal = vec2(cx - c.x, cy - c.x) + impact.yz; + + float maxBlur = mix(0.002, 0.009, rainAmount); + float minBlur = 0.001; + float focus = mix(maxBlur - c.y * 0.5, minBlur, smoothstep(0.05, 0.2, c.x + impact.x * 0.5)); + + vec2 texel = 1.0 / max(resolution, vec2(1.0)); + vec2 distortion = normal * (0.35 + focus * 48.0 + impact.x * 1.6); + + vec3 base = + texture2D(tDiffuse, uv + distortion).rgb * 0.4 + + texture2D(tDiffuse, uv + distortion + vec2(texel.x * focus * 24.0, 0.0)).rgb * 0.15 + + texture2D(tDiffuse, uv + distortion - vec2(texel.x * focus * 24.0, 0.0)).rgb * 0.15 + + texture2D(tDiffuse, uv + distortion + vec2(0.0, texel.y * focus * 24.0)).rgb * 0.15 + + texture2D(tDiffuse, uv + distortion - vec2(0.0, texel.y * focus * 24.0)).rgb * 0.15; + + vec3 sharp = texture2D(tDiffuse, uv + distortion * 0.6).rgb; + vec3 col = mix(base, sharp, smoothstep(0.04, 0.22, c.x)); + + vec3 rainTint = vec3(0.86, 0.91, 0.95); + col = mix(col, col * 0.88 + rainTint * 0.12, clamp(c.x * 0.65 + c.y * 0.3 + impact.x * 0.4, 0.0, 1.0)); + col += c.y * rainTint * 0.08 * screenAmount; + col += impact.x * rainTint * 0.18 * screenAmount; + col += veil * rainTint * (0.12 + veilAmount * 0.18); + col = mix(col, col * 0.94 + rainTint * 0.06, veil * 0.18); + col = mix(texture2D(tDiffuse, uv).rgb, col, clamp(rainAmount * 1.1, 0.0, 1.0)); + + gl_FragColor = vec4(col, 1.0); + } + ` + }; } async initSky() { @@ -636,6 +882,9 @@ export class OceanScene { if (this.composer) { this.composer.setSize(window.innerWidth, window.innerHeight); } + if (this.rainPass) { + this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); + } } setSunElevation(value) { @@ -718,6 +967,41 @@ export class OceanScene { this.updateFog(); } + setRainEnabled(value) { + this.params.rainEnabled = value; + if (this.rainPass) { + this.rainPass.enabled = value; + } + } + + setRainScreenIntensity(value) { + this.params.rainScreenIntensity = value; + if (this.rainPass) { + this.rainPass.material.uniforms.screenIntensity.value = value; + } + } + + setRainVeilIntensity(value) { + this.params.rainVeilIntensity = value; + if (this.rainPass) { + this.rainPass.material.uniforms.veilIntensity.value = value; + } + } + + setRainDropSize(value) { + this.params.rainDropSize = value; + if (this.rainPass) { + this.rainPass.material.uniforms.dropSize.value = value; + } + } + + setRainSpeed(value) { + this.params.rainSpeed = value; + if (this.rainPass) { + this.rainPass.material.uniforms.rainSpeed.value = value; + } + } + updateClouds() { if (!this.cloudGroup) return; @@ -823,6 +1107,10 @@ export class OceanScene { if (this.vegetationSystem) { this.vegetationSystem.update(time); } + + if (this.rainPass) { + this.rainPass.material.uniforms.time.value = time; + } this.controls.update(); if (this.composer) { diff --git a/src/main.js b/src/main.js index 9c48ea3..7ab6508 100644 --- a/src/main.js +++ b/src/main.js @@ -42,6 +42,15 @@ function setupControls(oceanScene) { }); }; + const bindCheckbox = (id, setter) => { + const checkbox = document.getElementById(id); + if (!checkbox) return; + + checkbox.addEventListener('change', (e) => { + setter(e.target.checked); + }); + }; + bindSlider('sun-elevation', (value) => `${value.toFixed(1)}°`, (value) => oceanScene.setSunElevation(value)); bindSlider('sun-azimuth', (value) => `${value.toFixed(1)}°`, (value) => oceanScene.setSunAzimuth(value)); bindSlider('exposure', (value) => value.toFixed(2), (value) => oceanScene.setExposure(value)); @@ -55,6 +64,11 @@ 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('rain-screen-intensity', (value) => value.toFixed(2), (value) => oceanScene.setRainScreenIntensity(value)); + bindSlider('rain-veil-intensity', (value) => value.toFixed(2), (value) => oceanScene.setRainVeilIntensity(value)); + bindSlider('rain-drop-size', (value) => value.toFixed(2), (value) => oceanScene.setRainDropSize(value)); + bindSlider('rain-speed', (value) => value.toFixed(2), (value) => oceanScene.setRainSpeed(value)); + bindCheckbox('rain-enabled', (value) => oceanScene.setRainEnabled(value)); } main().catch(console.error);