diff --git a/examples/jsm/objects/Sky.js b/examples/jsm/objects/Sky.js index b38759a8825081..e5a63284427c05 100644 --- a/examples/jsm/objects/Sky.js +++ b/examples/jsm/objects/Sky.js @@ -72,7 +72,13 @@ Sky.SkyShader = { 'mieCoefficient': { value: 0.005 }, 'mieDirectionalG': { value: 0.8 }, 'sunPosition': { value: new Vector3() }, - 'up': { value: new Vector3( 0, 1, 0 ) } + 'up': { value: new Vector3( 0, 1, 0 ) }, + 'cloudScale': { value: 0.0002 }, + 'cloudSpeed': { value: 0.0001 }, + 'cloudCoverage': { value: 0.4 }, + 'cloudDensity': { value: 0.4 }, + 'cloudElevation': { value: 0.5 }, + 'time': { value: 0.0 } }, vertexShader: /* glsl */` @@ -156,6 +162,39 @@ Sky.SkyShader = { uniform float mieDirectionalG; uniform vec3 up; + uniform float cloudScale; + uniform float cloudSpeed; + uniform float cloudCoverage; + uniform float cloudDensity; + uniform float cloudElevation; + uniform float time; + + // Cloud noise functions + float hash( vec2 p ) { + return fract( sin( dot( p, vec2( 127.1, 311.7 ) ) ) * 43758.5453123 ); + } + + float noise( vec2 p ) { + vec2 i = floor( p ); + vec2 f = fract( p ); + f = f * f * ( 3.0 - 2.0 * f ); + float a = hash( i ); + float b = hash( i + vec2( 1.0, 0.0 ) ); + float c = hash( i + vec2( 0.0, 1.0 ) ); + float d = hash( i + vec2( 1.0, 1.0 ) ); + return mix( mix( a, b, f.x ), mix( c, d, f.x ), f.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 *= 2.0; + amplitude *= 0.5; + } + return value; + } // constants for atmospheric scattering const float pi = 3.141592653589793238462643383279502884197169; @@ -222,6 +261,42 @@ Sky.SkyShader = { vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 ); + // Clouds + if ( direction.y > 0.0 && cloudCoverage > 0.0 ) { + + // Project to cloud plane (higher elevation = clouds appear lower/closer) + float elevation = mix( 1.0, 0.1, cloudElevation ); + vec2 cloudUV = direction.xz / ( direction.y * elevation ); + cloudUV *= cloudScale; + cloudUV += time * cloudSpeed; + + // Multi-octave noise for fluffy clouds + float cloudNoise = fbm( cloudUV * 1000.0 ); + cloudNoise += 0.5 * fbm( cloudUV * 2000.0 + 3.7 ); + cloudNoise = cloudNoise * 0.5 + 0.5; + + // Apply coverage threshold + float cloudMask = smoothstep( 1.0 - cloudCoverage, 1.0 - cloudCoverage + 0.3, cloudNoise ); + + // Fade clouds near horizon (adjusted by elevation) + float horizonFade = smoothstep( 0.0, 0.1 + 0.2 * cloudElevation, direction.y ); + cloudMask *= horizonFade; + + // Cloud lighting based on sun position + float sunInfluence = dot( direction, vSunDirection ) * 0.5 + 0.5; + float daylight = max( 0.0, vSunDirection.y * 2.0 ); + + // Base cloud color affected by atmosphere + vec3 atmosphereColor = Lin * 0.04; + vec3 cloudColor = mix( vec3( 0.3 ), vec3( 1.0 ), daylight ); + cloudColor = mix( cloudColor, atmosphereColor + vec3( 1.0 ), sunInfluence * 0.5 ); + cloudColor *= vSunE * 0.00002; + + // Blend clouds with sky + texColor = mix( texColor, cloudColor, cloudMask * cloudDensity ); + + } + gl_FragColor = vec4( texColor, 1.0 ); #include diff --git a/examples/jsm/objects/SkyMesh.js b/examples/jsm/objects/SkyMesh.js index 466ec571346083..b61e04ed59d5ae 100644 --- a/examples/jsm/objects/SkyMesh.js +++ b/examples/jsm/objects/SkyMesh.js @@ -6,7 +6,7 @@ import { NodeMaterial } from 'three/webgpu'; -import { Fn, float, vec3, acos, add, mul, clamp, cos, dot, exp, max, mix, modelViewProjection, normalize, positionWorld, pow, smoothstep, sub, varyingProperty, vec4, uniform, cameraPosition } from 'three/tsl'; +import { Fn, float, vec2, vec3, acos, add, mul, clamp, cos, dot, exp, max, mix, modelViewProjection, normalize, positionWorld, pow, smoothstep, sub, varyingProperty, vec4, uniform, cameraPosition, fract, floor, sin, time, Loop, If } from 'three/tsl'; /** * Represents a skydome for scene backgrounds. Based on [A Practical Analytic Model for Daylight](https://www.researchgate.net/publication/220720443_A_Practical_Analytic_Model_for_Daylight) @@ -82,6 +82,41 @@ class SkyMesh extends Mesh { */ this.upUniform = uniform( new Vector3( 0, 1, 0 ) ); + /** + * The cloud scale uniform. + * + * @type {UniformNode} + */ + this.cloudScale = uniform( 0.0002 ); + + /** + * The cloud speed uniform. + * + * @type {UniformNode} + */ + this.cloudSpeed = uniform( 0.0001 ); + + /** + * The cloud coverage uniform. + * + * @type {UniformNode} + */ + this.cloudCoverage = uniform( 0.4 ); + + /** + * The cloud density uniform. + * + * @type {UniformNode} + */ + this.cloudDensity = uniform( 0.4 ); + + /** + * The cloud elevation uniform. + * + * @type {UniformNode} + */ + this.cloudElevation = uniform( 0.5 ); + /** * This flag can be used for type testing. * @@ -230,7 +265,83 @@ class SkyMesh extends Mesh { const sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos.add( 0.00002 ), cosTheta ); L0.addAssign( vSunE.mul( 19000.0 ).mul( Fex ).mul( sundisk ) ); - const texColor = add( Lin, L0 ).mul( 0.04 ).add( vec3( 0.0, 0.0003, 0.00075 ) ); + const texColor = add( Lin, L0 ).mul( 0.04 ).add( vec3( 0.0, 0.0003, 0.00075 ) ).toVar(); + + // Cloud noise functions + const hash = Fn( ( [ p ] ) => { + + return fract( sin( dot( p, vec2( 127.1, 311.7 ) ) ).mul( 43758.5453123 ) ); + + } ); + + const noise = Fn( ( [ p_immutable ] ) => { + + const p = vec2( p_immutable ).toVar(); + const i = floor( p ); + const f = fract( p ); + const ff = f.mul( f ).mul( sub( 3.0, f.mul( 2.0 ) ) ); + + const a = hash( i ); + const b = hash( add( i, vec2( 1.0, 0.0 ) ) ); + const c = hash( add( i, vec2( 0.0, 1.0 ) ) ); + const d = hash( add( i, vec2( 1.0, 1.0 ) ) ); + + return mix( mix( a, b, ff.x ), mix( c, d, ff.x ), ff.y ); + + } ); + + const fbm = Fn( ( [ p_immutable ] ) => { + + const p = vec2( p_immutable ).toVar(); + const value = float( 0.0 ).toVar(); + const amplitude = float( 0.5 ).toVar(); + + Loop( 5, () => { + + value.addAssign( amplitude.mul( noise( p ) ) ); + p.mulAssign( 2.0 ); + amplitude.mulAssign( 0.5 ); + + } ); + + return value; + + } ); + + // Clouds + If( direction.y.greaterThan( 0.0 ).and( this.cloudCoverage.greaterThan( 0.0 ) ), () => { + + // Project to cloud plane (higher elevation = clouds appear lower/closer) + const elevation = mix( 1.0, 0.1, this.cloudElevation ); + const cloudUV = direction.xz.div( direction.y.mul( elevation ) ).toVar(); + cloudUV.mulAssign( this.cloudScale ); + cloudUV.addAssign( time.mul( this.cloudSpeed ) ); + + // Multi-octave noise for fluffy clouds + const cloudNoise = fbm( cloudUV.mul( 1000.0 ) ).add( fbm( cloudUV.mul( 2000.0 ).add( 3.7 ) ).mul( 0.5 ) ).toVar(); + cloudNoise.assign( cloudNoise.mul( 0.5 ).add( 0.5 ) ); + + // Apply coverage threshold + const cloudMask = smoothstep( sub( 1.0, this.cloudCoverage ), sub( 1.0, this.cloudCoverage ).add( 0.3 ), cloudNoise ).toVar(); + + // Fade clouds near horizon (adjusted by elevation) + const horizonFade = smoothstep( 0.0, add( 0.1, mul( 0.2, this.cloudElevation ) ), direction.y ); + cloudMask.mulAssign( horizonFade ); + + // Cloud lighting based on sun position + const sunInfluence = dot( direction, vSunDirection ).mul( 0.5 ).add( 0.5 ); + const daylight = max( 0.0, vSunDirection.y.mul( 2.0 ) ); + + // Base cloud color affected by atmosphere + const atmosphereColor = Lin.mul( 0.04 ); + const cloudColor = mix( vec3( 0.3 ), vec3( 1.0 ), daylight ).toVar(); + cloudColor.assign( mix( cloudColor, atmosphereColor.add( vec3( 1.0 ) ), sunInfluence.mul( 0.5 ) ) ); + cloudColor.mulAssign( vSunE.mul( 0.00002 ) ); + + // Blend clouds with sky + texColor.assign( mix( texColor, cloudColor, cloudMask.mul( this.cloudDensity ) ) ); + + } ); return vec4( texColor, 1.0 ); diff --git a/examples/screenshots/webgl_shaders_ocean.jpg b/examples/screenshots/webgl_shaders_ocean.jpg index a25240c49948f4..00d681246b49ab 100644 Binary files a/examples/screenshots/webgl_shaders_ocean.jpg and b/examples/screenshots/webgl_shaders_ocean.jpg differ diff --git a/examples/screenshots/webgl_shaders_sky.jpg b/examples/screenshots/webgl_shaders_sky.jpg index f7fcbcf2d635f5..c5b14c0740e4ee 100644 Binary files a/examples/screenshots/webgl_shaders_sky.jpg and b/examples/screenshots/webgl_shaders_sky.jpg differ diff --git a/examples/screenshots/webgpu_ocean.jpg b/examples/screenshots/webgpu_ocean.jpg index 7dfcae264c3d2b..ed9d8fc96862d0 100644 Binary files a/examples/screenshots/webgpu_ocean.jpg and b/examples/screenshots/webgpu_ocean.jpg differ diff --git a/examples/screenshots/webgpu_sky.jpg b/examples/screenshots/webgpu_sky.jpg index 09f3b0728fe89e..b65eb5a25422a5 100644 Binary files a/examples/screenshots/webgpu_sky.jpg and b/examples/screenshots/webgpu_sky.jpg differ diff --git a/examples/webgl_shaders_ocean.html b/examples/webgl_shaders_ocean.html index 3b11646bcb092d..94688032e6852d 100644 --- a/examples/webgl_shaders_ocean.html +++ b/examples/webgl_shaders_ocean.html @@ -36,7 +36,7 @@ let container, stats; let camera, scene, renderer; - let controls, water, sun, mesh, bloomPass; + let controls, water, sun, sky, mesh, bloomPass; init(); @@ -99,7 +99,7 @@ // Skybox - const sky = new Sky(); + sky = new Sky(); sky.scale.setScalar( 10000 ); scene.add( sky ); @@ -109,6 +109,9 @@ skyUniforms[ 'rayleigh' ].value = 2; skyUniforms[ 'mieCoefficient' ].value = 0.005; skyUniforms[ 'mieDirectionalG' ].value = 0.8; + skyUniforms[ 'cloudCoverage' ].value = 0.4; + skyUniforms[ 'cloudDensity' ].value = 0.5; + skyUniforms[ 'cloudElevation' ].value = 0.5; const parameters = { elevation: 2, @@ -191,6 +194,12 @@ folderBloom.add( bloomPass, 'radius', 0, 1, 0.01 ); folderBloom.open(); + const folderClouds = gui.addFolder( 'Clouds' ); + folderClouds.add( skyUniforms.cloudCoverage, 'value', 0, 1, 0.01 ).name( 'coverage' ); + folderClouds.add( skyUniforms.cloudDensity, 'value', 0, 1, 0.01 ).name( 'density' ); + folderClouds.add( skyUniforms.cloudElevation, 'value', 0, 1, 0.01 ).name( 'elevation' ); + folderClouds.open(); + // window.addEventListener( 'resize', onWindowResize ); @@ -222,6 +231,7 @@ mesh.rotation.z = time * 0.51; water.material.uniforms[ 'time' ].value += 1.0 / 60.0; + sky.material.uniforms[ 'time' ].value = time; renderer.render( scene, camera ); diff --git a/examples/webgl_shaders_sky.html b/examples/webgl_shaders_sky.html index 817aa1d1eb0d4f..7d01d8b60de0e1 100644 --- a/examples/webgl_shaders_sky.html +++ b/examples/webgl_shaders_sky.html @@ -33,7 +33,6 @@ let sky, sun; init(); - render(); function initSky() { @@ -53,7 +52,10 @@ mieDirectionalG: 0.7, elevation: 2, azimuth: 180, - exposure: renderer.toneMappingExposure + exposure: renderer.toneMappingExposure, + cloudCoverage: 0.4, + cloudDensity: 0.4, + cloudElevation: 0.5 }; function guiChanged() { @@ -63,6 +65,9 @@ uniforms[ 'rayleigh' ].value = effectController.rayleigh; uniforms[ 'mieCoefficient' ].value = effectController.mieCoefficient; uniforms[ 'mieDirectionalG' ].value = effectController.mieDirectionalG; + uniforms[ 'cloudCoverage' ].value = effectController.cloudCoverage; + uniforms[ 'cloudDensity' ].value = effectController.cloudDensity; + uniforms[ 'cloudElevation' ].value = effectController.cloudElevation; const phi = THREE.MathUtils.degToRad( 90 - effectController.elevation ); const theta = THREE.MathUtils.degToRad( effectController.azimuth ); @@ -72,7 +77,6 @@ uniforms[ 'sunPosition' ].value.copy( sun ); renderer.toneMappingExposure = effectController.exposure; - renderer.render( scene, camera ); } @@ -86,6 +90,11 @@ gui.add( effectController, 'azimuth', - 180, 180, 0.1 ).onChange( guiChanged ); gui.add( effectController, 'exposure', 0, 1, 0.0001 ).onChange( guiChanged ); + const folderClouds = gui.addFolder( 'Clouds' ); + folderClouds.add( effectController, 'cloudCoverage', 0, 1, 0.01 ).name( 'coverage' ).onChange( guiChanged ); + folderClouds.add( effectController, 'cloudDensity', 0, 1, 0.01 ).name( 'density' ).onChange( guiChanged ); + folderClouds.add( effectController, 'cloudElevation', 0, 1, 0.01 ).name( 'elevation' ).onChange( guiChanged ); + guiChanged(); } @@ -103,12 +112,12 @@ renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); + renderer.setAnimationLoop( animate ); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 0.5; document.body.appendChild( renderer.domElement ); const controls = new OrbitControls( camera, renderer.domElement ); - controls.addEventListener( 'change', render ); //controls.maxPolarAngle = Math.PI / 2; controls.enableZoom = false; controls.enablePan = false; @@ -126,12 +135,11 @@ renderer.setSize( window.innerWidth, window.innerHeight ); - render(); - } - function render() { + function animate() { + sky.material.uniforms[ 'time' ].value = performance.now() * 0.001; renderer.render( scene, camera ); } diff --git a/examples/webgpu_ocean.html b/examples/webgpu_ocean.html index 2df49543c57530..26618061d957d0 100644 --- a/examples/webgpu_ocean.html +++ b/examples/webgpu_ocean.html @@ -47,7 +47,7 @@ let container; let camera, scene, renderer, postProcessing; - let controls, water, sun, mesh, bloomPass; + let controls, water, sun, sky, mesh, bloomPass; init(); @@ -115,7 +115,7 @@ // Skybox - const sky = new SkyMesh(); + sky = new SkyMesh(); sky.scale.setScalar( 10000 ); scene.add( sky ); @@ -123,6 +123,9 @@ sky.rayleigh.value = 2; sky.mieCoefficient.value = 0.005; sky.mieDirectionalG.value = 0.8; + sky.cloudCoverage.value = 0.4; + sky.cloudDensity.value = 0.5; + sky.cloudElevation.value = 0.5; const parameters = { elevation: 2, @@ -195,6 +198,11 @@ folderBloom.add( bloomPass.strength, 'value', 0, 3, 0.01 ).name( 'strength' ); folderBloom.add( bloomPass.radius, 'value', 0, 1, 0.01 ).name( 'radius' ); + const folderClouds = gui.addFolder( 'Clouds' ); + folderClouds.add( sky.cloudCoverage, 'value', 0, 1, 0.01 ).name( 'coverage' ); + folderClouds.add( sky.cloudDensity, 'value', 0, 1, 0.01 ).name( 'density' ); + folderClouds.add( sky.cloudElevation, 'value', 0, 1, 0.01 ).name( 'elevation' ); + // window.addEventListener( 'resize', onWindowResize ); diff --git a/examples/webgpu_sky.html b/examples/webgpu_sky.html index 6610da08086276..78f5e563f62571 100644 --- a/examples/webgpu_sky.html +++ b/examples/webgpu_sky.html @@ -64,7 +64,10 @@ mieDirectionalG: 0.7, elevation: 2, azimuth: 180, - exposure: renderer.toneMappingExposure + exposure: renderer.toneMappingExposure, + cloudCoverage: 0.4, + cloudDensity: 0.4, + cloudElevation: 0.5 }; function guiChanged() { @@ -73,6 +76,9 @@ sky.rayleigh.value = effectController.rayleigh; sky.mieCoefficient.value = effectController.mieCoefficient; sky.mieDirectionalG.value = effectController.mieDirectionalG; + sky.cloudCoverage.value = effectController.cloudCoverage; + sky.cloudDensity.value = effectController.cloudDensity; + sky.cloudElevation.value = effectController.cloudElevation; const phi = THREE.MathUtils.degToRad( 90 - effectController.elevation ); const theta = THREE.MathUtils.degToRad( effectController.azimuth ); @@ -95,6 +101,11 @@ gui.add( effectController, 'azimuth', - 180, 180, 0.1 ).onChange( guiChanged ); gui.add( effectController, 'exposure', 0, 1, 0.0001 ).onChange( guiChanged ); + const folderClouds = gui.addFolder( 'Clouds' ); + folderClouds.add( effectController, 'cloudCoverage', 0, 1, 0.01 ).name( 'coverage' ).onChange( guiChanged ); + folderClouds.add( effectController, 'cloudDensity', 0, 1, 0.01 ).name( 'density' ).onChange( guiChanged ); + folderClouds.add( effectController, 'cloudElevation', 0, 1, 0.01 ).name( 'elevation' ).onChange( guiChanged ); + guiChanged(); }