Dynamic Cloud and Rain Shaders for CesiumJS Using Post-Processing
Preface:
When using Cesium for 3D visualization development, it is often necessary to add weather atmosphere to the scene. However, Cesium itself does not have built-in weather effects like overcast skies or rain. To implement these, you need to use PostProcessStage to run a custom fragment shader on the already rendered image for secondary processing.
This article implements two independently switchable effects:
- Dynamic Flowing Dark Clouds: Multi-layer FBM noise simulates cloud layers, supporting 0‑1 density adjustment.
- Rain Streak Effect: Randomly generated falling rain streak animation, with a slider to control rain intensity.
The Cesium version used in this case is 1.141.0
The dynamic demo effect is as follows:
The static demo effects are as follows:
1. Dark Cloud Effect
2. Rain Effect
3. Rain with Dark Cloud Effect
Detailed Technical Principles:
1. Cesium Post-Processing Execution Flow
Cesium scene rendering order: Scene Rendering → Obtain screen texture colorTexture → Execute custom fragment shader → Output final image
All our dark cloud and rain effects are secondary pixel redraws on the final screen image.
2. Dark Cloud Effect Implementation Idea
- Use FBM fractal noise with multiple layers superimposed to simulate realistic cloud textures.
- Dynamically offset UV coordinates via
timeto achieve slow cloud flow. - Use a
skyMaskpixel mask to render clouds only on the sky, leaving the ground completely unaffected. - Pass density externally via
uniformto support dynamic thickening/thinning.
3. Rain Effect Implementation Idea
- Use a Hash random algorithm to generate random rain streak positions on the screen.
- Use a time variable to drive the rain streak falling animation.
- Mix the image via a rain intensity coefficient to achieve a transition from light rain to heavy rain.
- Compatible with the GLSL300-ES standard, adapting to the new version of Cesium.
Complete Code
1. Template Structure:
<template>
<div class="main">
<!-- Cesium Globe Rendering Container -->
<div class="content" ref="content" id="earth"></div>
<div class="btn-border-column" v-if="isLoading">
<!-- Dark Cloud Control Row: Slider + Switch Button -->
<div class="btn-row">
<div class="slider-border" v-if="isCloud">
<div class="slider-label">Cloud Density: {{ Number(cloudStrength).toFixed(2) }}</div>
<el-slider
class="cloud-slider"
v-model.number="cloudStrength"
:min="0"
:max="1.0"
:step="0.01"
@input="updateCloudStrength"
/>
</div>
<el-button type="primary" size="default" class="btn-control" @click="cloudControl">
{{ isCloud ? 'Close Overcast Clouds' : 'Open Overcast Clouds' }}
</el-button>
</div>
<!-- Rain Control Row: Slider + Switch Button -->
<div class="btn-row">
<div class="slider-border" v-if="isRain">
<div class="slider-label">Rain Intensity: {{ Number(rainStrength).toFixed(2) }}</div>
<el-slider
class="rain-slider"
v-model.number="rainStrength"
:min="0"
:max="1.0"
:step="0.01"
@input="updateRainStrength"
/>
</div>
<el-button type="primary" size="default" class="btn-control" @click="rainControl">{{ isRain ? 'Close Rain' : 'Open Rain' }}</el-button>
</div>
<!-- Camera Reset Button -->
<div class="btn-row">
<el-button type="primary" size="default" class="btn-control" @click="flyTo">Initial Position</el-button>
</div>
</div>
<!-- Map Loading Prompt, displayed when initialization is not complete -->
<div class="loading" v-if="!isLoading">Loading...</div>
</div>
</template>
2. Script Code:
<script setup>
import { onMounted, nextTick, ref, onUnmounted } from 'vue';
import { token } from '../../utils/common.js';
// Whether the map has finished loading
let isLoading = ref(false);
// Dark cloud effect toggle state
let isCloud = ref(false);
// Rain effect toggle state
let isRain = ref(false);
// Cloud density 0~1
let cloudStrength = ref(1);
// Rain intensity 0~1
let rainStrength = ref(0.55);
let myMar = null;
/**
* Component unmount lifecycle: uniformly destroy Cesium instance and post-processing effects to prevent memory leaks
*/
onUnmounted(() => {
// Destroy dark cloud post-processing
if (isCloud.value && window.cloudStage) {
window.viewer.scene.postProcessStages.remove(window.cloudStage);
window.cloudStage = null;
isCloud.value = false;
}
// Destroy rain post-processing
if (isRain.value && window.rain) {
window.viewer.scene.postProcessStages.remove(window.rain);
window.rain = null;
isRain.value = false;
}
// Destroy Cesium instance
if (window.viewer) {
window.viewer.destroy();
window.viewer = null;
}
if (myMar) {
clearTimeout(myMar);
myMar = null;
}
});
/**
* DOM mount complete, wait for DOM rendering to finish before initializing the map
*/
onMounted(() => {
nextTick(() => {
initMap();
});
});
/**
* Initialize Cesium Viewer instance
*/
const initMap = async () => {
Cesium.Ion.defaultAccessToken = token;
// Set camera default viewport range
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);
// Load world terrain, enable water mask and terrain normals
const terrainProvider = await Cesium.createWorldTerrainAsync({
requestWaterMask: true,
requestVertexNormals: true
});
// Create Viewer instance, disable redundant UI controls
window.viewer = new Cesium.Viewer('earth', {
terrainProvider: terrainProvider,
animation: false,
timeline: false,
infoBox: false,
geocoder: false,
homeButton: false,
sceneModePicker: false,
baseLayerPicker: false,
navigationHelpButton: false,
fullscreenButton: false,
selectionIndicator: false,
shouldAnimate: false,
contextOptions: {
webgl: {
powerPreference: "high-performance",
preserveDrawingBuffer: false
}
}
});
Cesium.JulianDate.fromDate(new Date('2026/05/02 23:00:00'));
// Delay 3 seconds, mark map loading complete, display operation buttons, and fly to preset initial view
myMar = setTimeout(() => {
isLoading.value = true;
flyTo();
}, 3000);
};
// Method to fly to the preset initial view
const flyTo = () => {
window.viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(117.66293312354773, 26.00085216052459, 1796.8781247739746),
orientation: {
heading: Cesium.Math.toRadians(38.280907385928664),
pitch: Cesium.Math.toRadians(-4.0671391165843245),
roll: Cesium.Math.toRadians(0.0009439239381974838)
},
duration: 6
});
};
// Update rain uniform, slider drag takes effect in real-time, rain value 0‑1
const updateRainStrength = (val) => {
if (window.rain) {
window.rain.uniforms.rainStrength = Number(val);
}
};
/**
* Rain effect switch: enable/disable post-processing
*/
const rainControl = () => {
if (isRain.value) {
// Close rain, remove post-processing stage
window.viewer.scene.postProcessStages.remove(window.rain);
window.rain = null;
isRain.value = false;
} else {
// GLSL300‑ES fragment shader, Cesium PostProcessStage specific syntax
const Rain = `
uniform sampler2D colorTexture;
uniform float rainStrength;
uniform float time;
in vec2 v_textureCoordinates;
out vec4 fragColor;
float hash(float x) {
return fract(sin(x * 133.3) * 13.13);
}
void main(void) {
vec4 sceneColor = texture(colorTexture, v_textureCoordinates);
float t = time;
vec2 uv = gl_FragCoord.xy;
float a = -0.4;
float si = sin(a);
float co = cos(a);
vec2 res = czm_viewport.zw;
uv = (uv * 2.0 - res.xy) / min(res.x, res.y);
uv *= mat2(co, -si, si, co);
uv *= length(uv + vec2(0.0,4.9)) * 0.3 + 1.0;
float v = 1.0 - sin(hash(floor(uv.x * 100.0)) * 2.0);
float b = clamp(abs(sin(20.0 * t * v + uv.y * (5.0 / (2.0 + v)))) - 0.95, 0.0, 1.0) * 20.0;
vec3 rainCol = vec3(0.6,0.7,0.8) * v * b;
float rainMix = clamp(rainStrength,0.0,1.0);
vec3 finalRGB = mix(sceneColor.rgb, sceneColor.rgb * (1.0 - rainMix*0.2) + rainCol, rainMix*0.45);
fragColor = vec4(finalRGB, sceneColor.a);
}
`;
// Create rain post-processing
window.rain = new Cesium.PostProcessStage({
name: 'czm_rain',
fragmentShader: Rain,
uniforms:{
rainStrength: rainStrength.value,
time:0.0
}
});
// requestAnimationFrame drives time to achieve raindrop falling animation
const tickRain = ()=>{
if(window.rain){
window.rain.uniforms.time += 0.016;
requestAnimationFrame(tickRain);
}
};
tickRain();
// Add to scene post-processing pipeline
window.viewer.scene.postProcessStages.add(window.rain);
isRain.value = true;
}
};
// Update cloud density, slider drag modifies uniform in real-time, cloud density 0‑1
const updateCloudStrength = (val) => {
if(window.cloudStage){
window.cloudStage.uniforms.cloudStrength = Number(val);
}
};
// Dark cloud overcast effect switch
const cloudControl = () => {
if(isCloud.value){
// // Close dark clouds, remove post-processing
window.viewer.scene.postProcessStages.remove(window.cloudStage);
window.cloudStage = null;
isCloud.value = false;
}else{
const CloudShader = `
uniform sampler2D colorTexture;
uniform float cloudStrength;
uniform float time;
in vec2 v_textureCoordinates;
out vec4 fragColor;
float noise(vec2 uv){
return fract(sin(dot(uv,vec2(12.9898,78.233)))*43758.5453);
}
float smoothNoise(vec2 uv){
vec2 i = floor(uv);
vec2 f = fract(uv);
float a = noise(i);
float b = noise(i + vec2(1.,0.));
float c = noise(i + vec2(0.,1.));
float d = noise(i + vec2(1.,1.));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
}
float fbm(vec2 uv){
float total = 0.0;
float amp = 0.52;
for(int i = 0; i < 6; i++){
total += smoothNoise(uv) * amp;
uv *= 2.2;
amp *= 0.46;
}
return total;
}
void main(void){
vec4 origin = texture(colorTexture, v_textureCoordinates);
vec3 originRGB = origin.rgb;
float skyMask = smoothstep(0.12, 0.98, v_textureCoordinates.y);
vec2 cloudUV = v_textureCoordinates * 1.6 + vec2(time * 0.00045, time * 0.00018);
float cloudNoise = fbm(cloudUV);
float cloudDensity = smoothstep(0.04, 0.94, cloudNoise);
vec2 cloudUV2 = v_textureCoordinates * 2.9 + vec2(time * 0.0008, time * -0.0003);
float cloudNoise2 = fbm(cloudUV2);
float cloudDensity2 = smoothstep(0.06, 0.93, cloudNoise2);
vec2 cloudUV3 = v_textureCoordinates * 4.2 + vec2(time * 0.0010, time * 0.00022);
float cloudNoise3 = fbm(cloudUV3);
float cloudDensity3 = smoothstep(0.07, 0.91, cloudNoise3);
vec2 cloudUV4 = v_textureCoordinates * 6.0 + vec2(time * 0.0013, time * -0.0004);
float cloudNoise4 = fbm(cloudUV4);
float cloudDensity4 = smoothstep(0.09, 0.88, cloudNoise4);
float finalCloudDensity = max(cloudDensity, max(cloudDensity2*0.75, max(cloudDensity3*0.60, cloudDensity4*0.45)));
vec3 cloudGray = vec3(0.17, 0.19, 0.22);
float cloudAlpha = finalCloudDensity * skyMask * cloudStrength;
vec3 finalColor = mix(originRGB, cloudGray, cloudAlpha);
fragColor = vec4(finalColor, origin.a);
}
`;
// Create dark cloud post-processing stage
window.cloudStage = new Cesium.PostProcessStage({
name:'overcast_cloud',
fragmentShader:CloudShader,
uniforms:{
cloudStrength: cloudStrength.value,
time:0.0
}
});
// Drive cloud flow animation
const tick = ()=>{
if(window.cloudStage){
window.cloudStage.uniforms.time +=0.016;
requestAnimationFrame(tick);
}
};
tick();
window.viewer.scene.postProcessStages.add(window.cloudStage);
isCloud.value = true;
}
};
</script>
3. CSS Style Code:
* {
margin: 0;
padding: 0;
}
.main {
width: 100%;
height: 100vh;
position: relative;
}
.content {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
}
.btn-border-column {
position: absolute;
right: 24px;
top: 24px;
z-index: 2;
}
.btn-row {
margin-top: 10px;
height: 52px;
display: flex;
justify-content: end;
align-items: stretch;
}
.slider-border {
width: 260px;
margin-right: 20px;
position: relative;
top: -9px;
}
.slider-label {
font-size: 14px;
}
.btn-control {
width: 116px;
margin-left: 20px;
cursor: pointer;
}
.loading {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 3;
display: flex;
justify-content: center;
align-items: center;
font-size: 34px;
display: flex;
justify-content: center;
align-items: center;
font-size: 50px;
color: #000000;
}