diff --git a/src/OceanScene.js b/src/OceanScene.js index ab50182..a524e1b 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -520,6 +520,7 @@ export class OceanScene { async initSky() { this.sky = new Sky(); this.sky.scale.setScalar(10000); + this.sky.rotation.y = Math.PI; this.scene.add(this.sky); const skyUniforms = this.sky.material.uniforms; @@ -532,24 +533,55 @@ export class OceanScene { } initClouds() { - const cloudTexture = this.createCloudTexture(); this.cloudGroup = new THREE.Group(); - this.cloudGroup.position.y = -120; - this.addCloudDomeLayer(cloudTexture, { - radius: 4200, + this.cloudGroup.position.y = 40; + this.addCloudPlaneLayer({ + radius: 5200, + y: 120, opacity: 0.42, - repeatX: 5.5, - repeatY: 2.4, - speedX: 0.00045, - speedY: 0.00012 + scale: 1450, + detailScale: 3.4, + softness: 0.19, + edgeFade: 0.2, + shadowStrength: 0.36, + highlightStrength: 0.18, + erosionStrength: 0.2, + ridgeStrength: 0.08, + driftX: 0.0045, + driftY: 0.0012, + rotationZ: 0.06 }); - this.addCloudDomeLayer(cloudTexture, { - radius: 3900, + this.addCloudPlaneLayer({ + radius: 4300, + y: 250, opacity: 0.28, - repeatX: 7.5, - repeatY: 3.2, - speedX: -0.00032, - speedY: 0.00018 + scale: 980, + detailScale: 4.1, + softness: 0.17, + edgeFade: 0.24, + shadowStrength: 0.42, + highlightStrength: 0.24, + erosionStrength: 0.28, + ridgeStrength: 0.12, + driftX: -0.0032, + driftY: 0.0018, + rotationZ: -0.04 + }); + this.addCloudPlaneLayer({ + radius: 3400, + y: 360, + opacity: 0.18, + scale: 760, + detailScale: 4.8, + softness: 0.16, + edgeFade: 0.28, + shadowStrength: 0.46, + highlightStrength: 0.3, + erosionStrength: 0.34, + ridgeStrength: 0.16, + driftX: 0.0021, + driftY: -0.0014, + rotationZ: 0.1 }); this.scene.add(this.cloudGroup); @@ -579,44 +611,151 @@ export class OceanScene { this.scene.add(this.lightningCloudGlow); } - addCloudDomeLayer(texture, config) { - const layerTexture = texture.clone(); - layerTexture.wrapS = THREE.RepeatWrapping; - layerTexture.wrapT = THREE.RepeatWrapping; - layerTexture.repeat.set(config.repeatX, config.repeatY); - layerTexture.needsUpdate = true; + addCloudPlaneLayer(config) { + const material = new THREE.ShaderMaterial({ + uniforms: { + tintColor: { value: new THREE.Color(0xffffff) }, + layerOpacity: { value: config.opacity }, + time: { value: 0 }, + cloudCoverage: { value: this.params.cloudCoverage }, + cloudDensity: { value: this.params.cloudDensity }, + scale: { value: config.scale }, + detailScale: { value: config.detailScale }, + softness: { value: config.softness }, + edgeFade: { value: config.edgeFade }, + drift: { value: new THREE.Vector2(config.driftX, config.driftY) }, + seed: { value: Math.random() * 100.0 }, + lightDir: { value: new THREE.Vector2(1, 0) }, + shadowStrength: { value: config.shadowStrength ?? 0.42 }, + highlightStrength: { value: config.highlightStrength ?? 0.26 }, + erosionStrength: { value: config.erosionStrength ?? 0.24 }, + ridgeStrength: { value: config.ridgeStrength ?? 0.12 } + }, + vertexShader: ` + varying vec2 vPlaneUv; + varying vec3 vWorldPos; - const material = new THREE.MeshBasicMaterial({ - map: layerTexture, - alphaMap: layerTexture, - color: 0xffffff, + void main() { + vPlaneUv = uv; + vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform vec3 tintColor; + uniform float layerOpacity; + uniform float time; + uniform float cloudCoverage; + uniform float cloudDensity; + uniform float scale; + uniform float detailScale; + uniform float softness; + uniform float edgeFade; + uniform vec2 drift; + uniform float seed; + uniform vec2 lightDir; + uniform float shadowStrength; + uniform float highlightStrength; + uniform float erosionStrength; + uniform float ridgeStrength; + + varying vec2 vPlaneUv; + varying vec3 vWorldPos; + + float hash(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 78.233); + return fract(p.x * p.y); + } + + float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + + return mix( + mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x), + mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), + u.y + ); + } + + float fbm(vec2 p) { + float value = 0.0; + float amplitude = 0.5; + for (int i = 0; i < 5; i++) { + value += amplitude * noise(p); + p = p * 2.03 + vec2(11.7, 7.3); + amplitude *= 0.5; + } + return value; + } + + void main() { + vec2 flow = vWorldPos.xz / scale + drift * time + vec2(seed); + float base = fbm(flow); + float detail = fbm(flow * detailScale + vec2(0.0, time * 0.01)); + float wisps = fbm(flow * (detailScale * 0.55) - vec2(time * 0.008, 0.0)); + float billow = fbm(flow * 0.62 + vec2(seed * 0.37, -seed * 0.21)); + float erosion = fbm(flow * (detailScale * 1.8) + vec2(seed * 1.3, -seed * 0.9)); + float ridge = 1.0 - abs(fbm(flow * (detailScale * 0.85) - vec2(seed * 0.6, seed * 0.45)) * 2.0 - 1.0); + float shape = base * 0.5 + billow * 0.24 + detail * 0.14 + wisps * 0.12; + shape -= erosion * erosionStrength; + shape += ridge * ridgeStrength; + + float densityBoost = mix(0.78, 1.28, clamp(cloudDensity, 0.0, 1.0)); + float coverageThreshold = mix(0.84, 0.34, clamp(cloudCoverage, 0.0, 1.0)); + float alpha = smoothstep( + coverageThreshold + softness, + coverageThreshold - softness, + shape * densityBoost + ); + + float core = smoothstep( + coverageThreshold + softness * 0.4, + coverageThreshold - softness * 1.4, + shape * densityBoost + ); + vec2 eps = vec2(0.03, 0.0); + float sampleA = fbm((flow + lightDir * eps.x) * 0.62 + vec2(seed * 0.37, -seed * 0.21)); + float sampleB = fbm((flow - lightDir * eps.x) * 0.62 + vec2(seed * 0.37, -seed * 0.21)); + float lightEdge = clamp((sampleA - sampleB) * 3.2 + 0.5, 0.0, 1.0); + float underside = 1.0 - smoothstep(0.32, 0.88, shape); + + float radial = distance(vPlaneUv, vec2(0.5)) * 2.0; + float edgeMask = 1.0 - smoothstep(1.0 - edgeFade, 1.0, radial); + alpha *= edgeMask * layerOpacity; + if (alpha <= 0.001) discard; + + vec3 shadowColor = tintColor * (1.0 - shadowStrength); + vec3 litColor = mix(shadowColor, tintColor, core); + litColor += tintColor * lightEdge * core * highlightStrength; + litColor = mix(litColor, shadowColor, underside * shadowStrength * 0.7); + gl_FragColor = vec4(litColor, alpha); + } + `, transparent: true, - opacity: config.opacity, - side: THREE.BackSide, + side: THREE.DoubleSide, depthWrite: false, - alphaTest: 0.08, fog: false }); - const geometry = new THREE.SphereGeometry( - config.radius, - 32, - 20, - 0, - Math.PI * 2, - 0, - Math.PI * 0.48 - ); - + const geometry = new THREE.CircleGeometry(config.radius, 96); const mesh = new THREE.Mesh(geometry, material); - mesh.rotation.y = Math.random() * Math.PI * 2; - this.cloudMaterials.push(material); + mesh.rotation.x = -Math.PI / 2; + mesh.rotation.z = config.rotationZ ?? 0; + mesh.position.y = config.y; this.cloudLayers.push({ mesh, - texture: layerTexture, - speedX: config.speedX, - speedY: config.speedY, - baseOpacity: config.opacity + material, + baseOpacity: config.opacity, + baseY: config.y, + baseScale: config.scale, + baseSoftness: config.softness, + baseShadowStrength: config.shadowStrength ?? 0.42, + baseHighlightStrength: config.highlightStrength ?? 0.26, + baseErosionStrength: config.erosionStrength ?? 0.24, + baseRidgeStrength: config.ridgeStrength ?? 0.12 }); this.cloudGroup.add(mesh); } @@ -677,36 +816,6 @@ export class OceanScene { this.updateFog(); } - createCloudTexture() { - const canvas = document.createElement('canvas'); - canvas.width = 256; - canvas.height = 256; - - const context = canvas.getContext('2d'); - context.clearRect(0, 0, canvas.width, canvas.height); - - for (let i = 0; i < 28; i++) { - const x = 28 + Math.random() * 200; - const y = 52 + Math.random() * 152; - const radius = 28 + Math.random() * 44; - const gradient = context.createRadialGradient(x, y, 0, x, y, radius); - - gradient.addColorStop(0, 'rgba(255,255,255,0.92)'); - gradient.addColorStop(0.28, 'rgba(255,255,255,0.78)'); - gradient.addColorStop(0.62, 'rgba(255,255,255,0.22)'); - gradient.addColorStop(1, 'rgba(255,255,255,0)'); - - context.fillStyle = gradient; - context.fillRect(x - radius, y - radius, radius * 2, radius * 2); - } - - const texture = new THREE.CanvasTexture(canvas); - texture.colorSpace = THREE.SRGBColorSpace; - texture.needsUpdate = true; - - return texture; - } - createLightningGlowTexture() { const canvas = document.createElement('canvas'); canvas.width = 256; @@ -1122,7 +1231,7 @@ export class OceanScene { setCloudElevation(value) { this.params.cloudElevation = value; if (this.cloudGroup) { - this.cloudGroup.position.y = THREE.MathUtils.lerp(-380, 260, value); + this.cloudGroup.position.y = THREE.MathUtils.lerp(-160, 260, value); } } @@ -1484,20 +1593,48 @@ export class OceanScene { if (!this.cloudGroup) return; const sunMix = THREE.MathUtils.clamp((this.params.elevation + 10) / 100, 0, 1); + const dawnMix = 1.0 - THREE.MathUtils.smoothstep(this.params.elevation, 8, 34); + const rainMix = this.params.rainEnabled ? THREE.MathUtils.clamp(this.params.rainVeilIntensity / 2.0, 0.35, 1.0) : 0; + const snowMix = this.params.snowEnabled ? THREE.MathUtils.clamp(this.params.snowIntensity / 1.5, 0.35, 1.0) : 0; + const stormMix = Math.max(rainMix, snowMix); const warmCloud = new THREE.Color(0xdab188); const dayCloud = new THREE.Color(0xd1dbe6); const cloudColor = warmCloud.lerp(dayCloud, sunMix); const lightningMix = THREE.MathUtils.clamp(this.lightningFlash * 0.32, 0, 1); + cloudColor.lerp(new THREE.Color(0xbfcad6), stormMix * 0.45); cloudColor.lerp(new THREE.Color(0xe9f3ff), lightningMix); + const lightDir = new THREE.Vector2(this.sun.x, this.sun.z).normalize(); - for (const layer of this.cloudLayers) { + this.cloudLayers.forEach((layer, index) => { const coverageFactor = 0.15 + this.params.cloudCoverage * 1.15; const densityFactor = 0.2 + this.params.cloudDensity * 1.35; - const opacity = layer.baseOpacity * coverageFactor * densityFactor; - layer.mesh.material.opacity = opacity; - layer.mesh.material.color.copy(cloudColor); + const layerDepth = index / Math.max(this.cloudLayers.length - 1, 1); + const weatherOpacityBoost = THREE.MathUtils.lerp(1.0, 1.42 - layerDepth * 0.16, rainMix); + const snowOpacityBoost = THREE.MathUtils.lerp(1.0, 1.22 - layerDepth * 0.08, snowMix); + const opacity = layer.baseOpacity * coverageFactor * densityFactor * weatherOpacityBoost * snowOpacityBoost; + const heightDrop = THREE.MathUtils.lerp(0, 120 + index * 55, rainMix); + const snowLift = THREE.MathUtils.lerp(0, 40 + index * 24, snowMix); + const layerScale = layer.baseScale * THREE.MathUtils.lerp(1.18 - index * 0.06, 0.9 + index * 0.04, rainMix); + const softness = layer.baseSoftness * THREE.MathUtils.lerp(1.15, 0.82, rainMix) * THREE.MathUtils.lerp(1.0, 1.08, snowMix); + const erosion = layer.baseErosionStrength * THREE.MathUtils.lerp(0.72 + index * 0.08, 1.28 + index * 0.1, rainMix); + const ridge = layer.baseRidgeStrength * THREE.MathUtils.lerp(1.24, 0.78, rainMix) * THREE.MathUtils.lerp(1.0, 0.86, snowMix); + const highlight = layer.baseHighlightStrength * THREE.MathUtils.lerp(1.45 - index * 0.12, 0.62, rainMix) * THREE.MathUtils.lerp(1.12, 0.84, snowMix); + const shadow = layer.baseShadowStrength * THREE.MathUtils.lerp(0.94, 1.36, stormMix); + + layer.material.uniforms.layerOpacity.value = opacity; + layer.material.uniforms.cloudCoverage.value = this.params.cloudCoverage; + layer.material.uniforms.cloudDensity.value = this.params.cloudDensity; + layer.material.uniforms.scale.value = layerScale; + layer.material.uniforms.softness.value = softness; + layer.material.uniforms.erosionStrength.value = erosion; + layer.material.uniforms.ridgeStrength.value = ridge; + layer.material.uniforms.highlightStrength.value = highlight; + layer.material.uniforms.shadowStrength.value = shadow; + layer.material.uniforms.tintColor.value.copy(cloudColor); + layer.material.uniforms.lightDir.value.copy(lightDir); + layer.mesh.position.y = layer.baseY - heightDrop + snowLift + dawnMix * index * 18.0; layer.mesh.visible = opacity > 0.015; - } + }); } updateFog() { @@ -1571,10 +1708,8 @@ export class OceanScene { } if (this.cloudGroup) { - this.cloudGroup.rotation.y = time * 0.004; this.cloudLayers.forEach((layer) => { - layer.texture.offset.x = time * layer.speedX; - layer.texture.offset.y = time * layer.speedY; + layer.material.uniforms.time.value = time; }); }