Skip to content

Commit f848b75

Browse files
authored
Sky/SkyMesh: Added procedural clouds. (mrdoob#32682)
1 parent 40d7441 commit f848b75

10 files changed

Lines changed: 238 additions & 15 deletions

examples/jsm/objects/Sky.js

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,13 @@ Sky.SkyShader = {
7272
'mieCoefficient': { value: 0.005 },
7373
'mieDirectionalG': { value: 0.8 },
7474
'sunPosition': { value: new Vector3() },
75-
'up': { value: new Vector3( 0, 1, 0 ) }
75+
'up': { value: new Vector3( 0, 1, 0 ) },
76+
'cloudScale': { value: 0.0002 },
77+
'cloudSpeed': { value: 0.0001 },
78+
'cloudCoverage': { value: 0.4 },
79+
'cloudDensity': { value: 0.4 },
80+
'cloudElevation': { value: 0.5 },
81+
'time': { value: 0.0 }
7682
},
7783

7884
vertexShader: /* glsl */`
@@ -156,6 +162,39 @@ Sky.SkyShader = {
156162
157163
uniform float mieDirectionalG;
158164
uniform vec3 up;
165+
uniform float cloudScale;
166+
uniform float cloudSpeed;
167+
uniform float cloudCoverage;
168+
uniform float cloudDensity;
169+
uniform float cloudElevation;
170+
uniform float time;
171+
172+
// Cloud noise functions
173+
float hash( vec2 p ) {
174+
return fract( sin( dot( p, vec2( 127.1, 311.7 ) ) ) * 43758.5453123 );
175+
}
176+
177+
float noise( vec2 p ) {
178+
vec2 i = floor( p );
179+
vec2 f = fract( p );
180+
f = f * f * ( 3.0 - 2.0 * f );
181+
float a = hash( i );
182+
float b = hash( i + vec2( 1.0, 0.0 ) );
183+
float c = hash( i + vec2( 0.0, 1.0 ) );
184+
float d = hash( i + vec2( 1.0, 1.0 ) );
185+
return mix( mix( a, b, f.x ), mix( c, d, f.x ), f.y );
186+
}
187+
188+
float fbm( vec2 p ) {
189+
float value = 0.0;
190+
float amplitude = 0.5;
191+
for ( int i = 0; i < 5; i ++ ) {
192+
value += amplitude * noise( p );
193+
p *= 2.0;
194+
amplitude *= 0.5;
195+
}
196+
return value;
197+
}
159198
160199
// constants for atmospheric scattering
161200
const float pi = 3.141592653589793238462643383279502884197169;
@@ -222,6 +261,42 @@ Sky.SkyShader = {
222261
223262
vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );
224263
264+
// Clouds
265+
if ( direction.y > 0.0 && cloudCoverage > 0.0 ) {
266+
267+
// Project to cloud plane (higher elevation = clouds appear lower/closer)
268+
float elevation = mix( 1.0, 0.1, cloudElevation );
269+
vec2 cloudUV = direction.xz / ( direction.y * elevation );
270+
cloudUV *= cloudScale;
271+
cloudUV += time * cloudSpeed;
272+
273+
// Multi-octave noise for fluffy clouds
274+
float cloudNoise = fbm( cloudUV * 1000.0 );
275+
cloudNoise += 0.5 * fbm( cloudUV * 2000.0 + 3.7 );
276+
cloudNoise = cloudNoise * 0.5 + 0.5;
277+
278+
// Apply coverage threshold
279+
float cloudMask = smoothstep( 1.0 - cloudCoverage, 1.0 - cloudCoverage + 0.3, cloudNoise );
280+
281+
// Fade clouds near horizon (adjusted by elevation)
282+
float horizonFade = smoothstep( 0.0, 0.1 + 0.2 * cloudElevation, direction.y );
283+
cloudMask *= horizonFade;
284+
285+
// Cloud lighting based on sun position
286+
float sunInfluence = dot( direction, vSunDirection ) * 0.5 + 0.5;
287+
float daylight = max( 0.0, vSunDirection.y * 2.0 );
288+
289+
// Base cloud color affected by atmosphere
290+
vec3 atmosphereColor = Lin * 0.04;
291+
vec3 cloudColor = mix( vec3( 0.3 ), vec3( 1.0 ), daylight );
292+
cloudColor = mix( cloudColor, atmosphereColor + vec3( 1.0 ), sunInfluence * 0.5 );
293+
cloudColor *= vSunE * 0.00002;
294+
295+
// Blend clouds with sky
296+
texColor = mix( texColor, cloudColor, cloudMask * cloudDensity );
297+
298+
}
299+
225300
gl_FragColor = vec4( texColor, 1.0 );
226301
227302
#include <tonemapping_fragment>

examples/jsm/objects/SkyMesh.js

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
NodeMaterial
77
} from 'three/webgpu';
88

9-
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';
9+
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';
1010

1111
/**
1212
* 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 {
8282
*/
8383
this.upUniform = uniform( new Vector3( 0, 1, 0 ) );
8484

85+
/**
86+
* The cloud scale uniform.
87+
*
88+
* @type {UniformNode<float>}
89+
*/
90+
this.cloudScale = uniform( 0.0002 );
91+
92+
/**
93+
* The cloud speed uniform.
94+
*
95+
* @type {UniformNode<float>}
96+
*/
97+
this.cloudSpeed = uniform( 0.0001 );
98+
99+
/**
100+
* The cloud coverage uniform.
101+
*
102+
* @type {UniformNode<float>}
103+
*/
104+
this.cloudCoverage = uniform( 0.4 );
105+
106+
/**
107+
* The cloud density uniform.
108+
*
109+
* @type {UniformNode<float>}
110+
*/
111+
this.cloudDensity = uniform( 0.4 );
112+
113+
/**
114+
* The cloud elevation uniform.
115+
*
116+
* @type {UniformNode<float>}
117+
*/
118+
this.cloudElevation = uniform( 0.5 );
119+
85120
/**
86121
* This flag can be used for type testing.
87122
*
@@ -230,7 +265,83 @@ class SkyMesh extends Mesh {
230265
const sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos.add( 0.00002 ), cosTheta );
231266
L0.addAssign( vSunE.mul( 19000.0 ).mul( Fex ).mul( sundisk ) );
232267

233-
const texColor = add( Lin, L0 ).mul( 0.04 ).add( vec3( 0.0, 0.0003, 0.00075 ) );
268+
const texColor = add( Lin, L0 ).mul( 0.04 ).add( vec3( 0.0, 0.0003, 0.00075 ) ).toVar();
269+
270+
// Cloud noise functions
271+
const hash = Fn( ( [ p ] ) => {
272+
273+
return fract( sin( dot( p, vec2( 127.1, 311.7 ) ) ).mul( 43758.5453123 ) );
274+
275+
} );
276+
277+
const noise = Fn( ( [ p_immutable ] ) => {
278+
279+
const p = vec2( p_immutable ).toVar();
280+
const i = floor( p );
281+
const f = fract( p );
282+
const ff = f.mul( f ).mul( sub( 3.0, f.mul( 2.0 ) ) );
283+
284+
const a = hash( i );
285+
const b = hash( add( i, vec2( 1.0, 0.0 ) ) );
286+
const c = hash( add( i, vec2( 0.0, 1.0 ) ) );
287+
const d = hash( add( i, vec2( 1.0, 1.0 ) ) );
288+
289+
return mix( mix( a, b, ff.x ), mix( c, d, ff.x ), ff.y );
290+
291+
} );
292+
293+
const fbm = Fn( ( [ p_immutable ] ) => {
294+
295+
const p = vec2( p_immutable ).toVar();
296+
const value = float( 0.0 ).toVar();
297+
const amplitude = float( 0.5 ).toVar();
298+
299+
Loop( 5, () => {
300+
301+
value.addAssign( amplitude.mul( noise( p ) ) );
302+
p.mulAssign( 2.0 );
303+
amplitude.mulAssign( 0.5 );
304+
305+
} );
306+
307+
return value;
308+
309+
} );
310+
311+
// Clouds
312+
If( direction.y.greaterThan( 0.0 ).and( this.cloudCoverage.greaterThan( 0.0 ) ), () => {
313+
314+
// Project to cloud plane (higher elevation = clouds appear lower/closer)
315+
const elevation = mix( 1.0, 0.1, this.cloudElevation );
316+
const cloudUV = direction.xz.div( direction.y.mul( elevation ) ).toVar();
317+
cloudUV.mulAssign( this.cloudScale );
318+
cloudUV.addAssign( time.mul( this.cloudSpeed ) );
319+
320+
// Multi-octave noise for fluffy clouds
321+
const cloudNoise = fbm( cloudUV.mul( 1000.0 ) ).add( fbm( cloudUV.mul( 2000.0 ).add( 3.7 ) ).mul( 0.5 ) ).toVar();
322+
cloudNoise.assign( cloudNoise.mul( 0.5 ).add( 0.5 ) );
323+
324+
// Apply coverage threshold
325+
const cloudMask = smoothstep( sub( 1.0, this.cloudCoverage ), sub( 1.0, this.cloudCoverage ).add( 0.3 ), cloudNoise ).toVar();
326+
327+
// Fade clouds near horizon (adjusted by elevation)
328+
const horizonFade = smoothstep( 0.0, add( 0.1, mul( 0.2, this.cloudElevation ) ), direction.y );
329+
cloudMask.mulAssign( horizonFade );
330+
331+
// Cloud lighting based on sun position
332+
const sunInfluence = dot( direction, vSunDirection ).mul( 0.5 ).add( 0.5 );
333+
const daylight = max( 0.0, vSunDirection.y.mul( 2.0 ) );
334+
335+
// Base cloud color affected by atmosphere
336+
const atmosphereColor = Lin.mul( 0.04 );
337+
const cloudColor = mix( vec3( 0.3 ), vec3( 1.0 ), daylight ).toVar();
338+
cloudColor.assign( mix( cloudColor, atmosphereColor.add( vec3( 1.0 ) ), sunInfluence.mul( 0.5 ) ) );
339+
cloudColor.mulAssign( vSunE.mul( 0.00002 ) );
340+
341+
// Blend clouds with sky
342+
texColor.assign( mix( texColor, cloudColor, cloudMask.mul( this.cloudDensity ) ) );
343+
344+
} );
234345

235346
return vec4( texColor, 1.0 );
236347

-1.06 KB
Loading
288 Bytes
Loading
204 Bytes
Loading
330 Bytes
Loading

examples/webgl_shaders_ocean.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
let container, stats;
3838
let camera, scene, renderer;
39-
let controls, water, sun, mesh, bloomPass;
39+
let controls, water, sun, sky, mesh, bloomPass;
4040

4141
init();
4242

@@ -99,7 +99,7 @@
9999

100100
// Skybox
101101

102-
const sky = new Sky();
102+
sky = new Sky();
103103
sky.scale.setScalar( 10000 );
104104
scene.add( sky );
105105

@@ -109,6 +109,9 @@
109109
skyUniforms[ 'rayleigh' ].value = 2;
110110
skyUniforms[ 'mieCoefficient' ].value = 0.005;
111111
skyUniforms[ 'mieDirectionalG' ].value = 0.8;
112+
skyUniforms[ 'cloudCoverage' ].value = 0.4;
113+
skyUniforms[ 'cloudDensity' ].value = 0.5;
114+
skyUniforms[ 'cloudElevation' ].value = 0.5;
112115

113116
const parameters = {
114117
elevation: 2,
@@ -191,6 +194,12 @@
191194
folderBloom.add( bloomPass, 'radius', 0, 1, 0.01 );
192195
folderBloom.open();
193196

197+
const folderClouds = gui.addFolder( 'Clouds' );
198+
folderClouds.add( skyUniforms.cloudCoverage, 'value', 0, 1, 0.01 ).name( 'coverage' );
199+
folderClouds.add( skyUniforms.cloudDensity, 'value', 0, 1, 0.01 ).name( 'density' );
200+
folderClouds.add( skyUniforms.cloudElevation, 'value', 0, 1, 0.01 ).name( 'elevation' );
201+
folderClouds.open();
202+
194203
//
195204

196205
window.addEventListener( 'resize', onWindowResize );
@@ -222,6 +231,7 @@
222231
mesh.rotation.z = time * 0.51;
223232

224233
water.material.uniforms[ 'time' ].value += 1.0 / 60.0;
234+
sky.material.uniforms[ 'time' ].value = time;
225235

226236
renderer.render( scene, camera );
227237

examples/webgl_shaders_sky.html

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
let sky, sun;
3434

3535
init();
36-
render();
3736

3837
function initSky() {
3938

@@ -53,7 +52,10 @@
5352
mieDirectionalG: 0.7,
5453
elevation: 2,
5554
azimuth: 180,
56-
exposure: renderer.toneMappingExposure
55+
exposure: renderer.toneMappingExposure,
56+
cloudCoverage: 0.4,
57+
cloudDensity: 0.4,
58+
cloudElevation: 0.5
5759
};
5860

5961
function guiChanged() {
@@ -63,6 +65,9 @@
6365
uniforms[ 'rayleigh' ].value = effectController.rayleigh;
6466
uniforms[ 'mieCoefficient' ].value = effectController.mieCoefficient;
6567
uniforms[ 'mieDirectionalG' ].value = effectController.mieDirectionalG;
68+
uniforms[ 'cloudCoverage' ].value = effectController.cloudCoverage;
69+
uniforms[ 'cloudDensity' ].value = effectController.cloudDensity;
70+
uniforms[ 'cloudElevation' ].value = effectController.cloudElevation;
6671

6772
const phi = THREE.MathUtils.degToRad( 90 - effectController.elevation );
6873
const theta = THREE.MathUtils.degToRad( effectController.azimuth );
@@ -72,7 +77,6 @@
7277
uniforms[ 'sunPosition' ].value.copy( sun );
7378

7479
renderer.toneMappingExposure = effectController.exposure;
75-
renderer.render( scene, camera );
7680

7781
}
7882

@@ -86,6 +90,11 @@
8690
gui.add( effectController, 'azimuth', - 180, 180, 0.1 ).onChange( guiChanged );
8791
gui.add( effectController, 'exposure', 0, 1, 0.0001 ).onChange( guiChanged );
8892

93+
const folderClouds = gui.addFolder( 'Clouds' );
94+
folderClouds.add( effectController, 'cloudCoverage', 0, 1, 0.01 ).name( 'coverage' ).onChange( guiChanged );
95+
folderClouds.add( effectController, 'cloudDensity', 0, 1, 0.01 ).name( 'density' ).onChange( guiChanged );
96+
folderClouds.add( effectController, 'cloudElevation', 0, 1, 0.01 ).name( 'elevation' ).onChange( guiChanged );
97+
8998
guiChanged();
9099

91100
}
@@ -103,12 +112,12 @@
103112
renderer = new THREE.WebGLRenderer();
104113
renderer.setPixelRatio( window.devicePixelRatio );
105114
renderer.setSize( window.innerWidth, window.innerHeight );
115+
renderer.setAnimationLoop( animate );
106116
renderer.toneMapping = THREE.ACESFilmicToneMapping;
107117
renderer.toneMappingExposure = 0.5;
108118
document.body.appendChild( renderer.domElement );
109119

110120
const controls = new OrbitControls( camera, renderer.domElement );
111-
controls.addEventListener( 'change', render );
112121
//controls.maxPolarAngle = Math.PI / 2;
113122
controls.enableZoom = false;
114123
controls.enablePan = false;
@@ -126,12 +135,11 @@
126135

127136
renderer.setSize( window.innerWidth, window.innerHeight );
128137

129-
render();
130-
131138
}
132139

133-
function render() {
140+
function animate() {
134141

142+
sky.material.uniforms[ 'time' ].value = performance.now() * 0.001;
135143
renderer.render( scene, camera );
136144

137145
}

0 commit comments

Comments
 (0)