跪拜 Guibai
← Back to the summary

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:

  1. Dynamic Flowing Dark Clouds: Multi-layer FBM noise simulates cloud layers, supporting 0‑1 density adjustment.
  2. 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:

rain.gif

The static demo effects are as follows:

1. Dark Cloud Effect

wuyun.png

2. Rain Effect

rain.png

3. Rain with Dark Cloud Effect

rain_cover.png

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

3. Rain Effect Implementation Idea

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;
}