跪拜 Guibai
← Back to the summary

A Pure-Shader Sunset Glow for Cesium with a Real-Time Intensity Slider

Foreword:

This article will walk you through building a dynamic, flowing sunset glow effect with adjustable intensity and realistic evening hues from scratch, using Cesium's native PostProcessStage post-processing technique. It is pure shader-based, high-performance, and requires no third-party plugins.

The dynamic demo effect is shown below:

hou.gif

Effect Highlights:

Core Technical Principles:

Cesium Post-Processing Mechanism

Cesium provides the PostProcessStage screen post-processing stage, which allows us to apply a second pass to the entire screen texture via a custom fragment shader after the scene has finished rendering but before it is output to the screen. Weather effects, filters, bloom, color grading, and sky effects are all built on this principle.

Core Principle of the Fire Cloud Effect

A simple solid-color gradient cannot simulate real clouds. Real cloud layers are irregular, fragmented, and multi-layered textures.

This example uses the industry-standard FBM (Fractional Brownian Motion) noise:

Complete Runnable Source Code:

The following is a complete, directly runnable Vue3 + Cesium code, including UI controls and parameter adjustment.

1. Template structure:

<template>
    <div class="main">
        <!-- Cesium globe rendering container -->
        <div class="content" ref="content" id="earth"></div>

        <div class="btn-border" v-if="isLoading">

            <div class="slider-border" v-if="isFireCloud">
                <div class="slider-label">Fire Cloud Intensity: {{ Number(fireCloudStrength).toFixed(2) }}</div>
                <el-slider
                    class="firecloud-slider"
                    v-model.number="fireCloudStrength"
                    :min="0"
                    :max="0.8"
                    :step="0.01"
                    @input="updateFireCloudStrength"
                />
            </div>

            <!-- Camera reset button, returns to the preset camera view -->
            <el-button type="primary" size="default" class="btn" @click="flyTo">Initial Position</el-button>

            <!-- Fire cloud toggle button, switches the effect on/off -->
            <el-button type="primary" size="default" class="btn" @click="fireCloudControl">
              {{ isFireCloud ? 'Disable Fire Clouds' : 'Enable Fire Clouds' }}
            </el-button>

        </div>

        <!-- Map loading indicator, shown while 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 initializing and loading
let isLoading = ref(false);
// Fire cloud effect toggle state
let isFireCloud = ref(false);
// Fire cloud intensity, range 0 ~ 0.8
let fireCloudStrength = ref(0.8);

// Map loading delay timer handle
let myMar = null;

/**
 * Component unmount lifecycle hook
 * Destroys the Cesium instance and post-processing effects, releases GPU resources to avoid memory leaks
 */
onUnmounted(() => {
    // Destroy the fire cloud post-processing stage
    if (isFireCloud.value && window.fireCloudStage) {
        window.viewer.scene.postProcessStages.remove(window.fireCloudStage);
        window.fireCloudStage = null;
        isFireCloud.value = false;
    }
    // Destroy the Cesium Viewer instance
    if (window.viewer) {
        window.viewer.destroy();
        window.viewer = null;
    }
    // Clear the delay timer
    if (myMar) {
        clearTimeout(myMar);
        myMar = null;
    }
});

/**
 * DOM mounted hook
 * Waits for nextTick to ensure the DOM has rendered before initializing the map instance
 */
onMounted(() => {
    nextTick(() => {
        initMap();
    });
});

/**
 * Initialize the Cesium Viewer map instance
 */
const initMap = async () => {
    // Your Cesium Ion token
    Cesium.Ion.defaultAccessToken = token;

    // Set the default camera viewport rectangle range
    Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);

    // Load Cesium world terrain, enable water mask and terrain normals for water effects and lighting
    const terrainProvider = await Cesium.createWorldTerrainAsync({
        requestWaterMask: true,
        requestVertexNormals: true
    });

    // Create Viewer instance, disable all unnecessary UI widgets
    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
            }
        }
    });

    // Set the scene time (only assign, do not drive the clock)
    Cesium.JulianDate.fromDate(new Date('2026/05/02 23:00:00'));

    // Delay 3 seconds, mark map loading as complete, show operation buttons, and fly to the preset initial view
    myMar = setTimeout(() => {
        isLoading.value = true;
        flyTo();
    }, 3000);
};

/**
 * Fly the camera to the preset initial position
 */
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 the fire cloud intensity uniform; the slider drag updates the shader parameter in real time
 * @param {Number} val 0‑0.8 fire cloud intensity value
 */
const updateFireCloudStrength = (val) => {
    if (window.fireCloudStage) {
        window.fireCloudStage.uniforms.fireCloudStrength = Number(val);
    }
};

/**
 * Fire cloud effect toggle
 * Enable: Create a PostProcessStage, load the fragment shader, add it to the post-processing pipeline
 * Disable: Remove the post-processing stage, destroy the instance
 */
const fireCloudControl = () => {
    if (isFireCloud.value) {
        // Disable fire clouds, remove the post-processing stage
        window.viewer.scene.postProcessStages.remove(window.fireCloudStage);
        window.fireCloudStage = null;
        isFireCloud.value = false;
    } else {
        // Fire cloud fragment shader, using Cesium PostProcessStage-specific GLSL-ES300 syntax
        const FireCloudShader = `
            uniform sampler2D colorTexture;   // Original scene render texture
            uniform float fireCloudStrength;  // Fire cloud intensity 0‑0.8
            uniform float time;               // Time, used for cloud flow animation
            in vec2 v_textureCoordinates;      // Screen texture UV coordinates
            out vec4 fragColor;               // Output fragment color

            // Basic random noise function
            float noise(vec2 uv){
                return fract(sin(dot(uv,vec2(12.9898,78.233)))*43758.5453);
            }

            // Smooth interpolated noise
            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);
            }

            // Fractional Brownian Motion, multi-layer noise superposition to generate cloud texture
            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){
                // Get the original scene color
                vec4 origin = texture(colorTexture, v_textureCoordinates);
                vec3 originRGB = origin.rgb;

                // skyMask: Mask, only renders fire clouds in the sky/horizon region, not on the ground
                float skyMask = smoothstep(0.05, 0.75, v_textureCoordinates.y);

                // Multiple UV sets, different scaling + time offsets to achieve multi-layered cloud flow
                vec2 cloudUV = v_textureCoordinates *1.7 + vec2(time * 0.00035, time * 0.00012);
                float cloudNoise = fbm(cloudUV);
                float cloudDensity = smoothstep(0.1,0.92,cloudNoise);

                vec2 cloudUV2 = v_textureCoordinates *3.1 + vec2(time *0.0006,-time*0.0002);
                float cloudNoise2 = fbm(cloudUV2);
                float cloudDensity2 = smoothstep(0.12,0.9,cloudNoise2);

                vec2 cloudUV3 = v_textureCoordinates *4.8 + vec2(time*0.0009, time*0.00015);
                float cloudNoise3 = fbm(cloudUV3);
                float cloudDensity3 = smoothstep(0.14,0.88,cloudNoise3);

                // Combine multi-layer cloud densities
                float finalDensity = max(cloudDensity, max(cloudDensity2*0.72, cloudDensity3*0.55));

                // Fire cloud gradient tones: orange-red transitioning to golden-orange
                vec3 fireColorA = vec3(1.0,0.42,0.18);
                vec3 fireColorB = vec3(1.0,0.72,0.25);
                vec3 fireCloudColor = mix(fireColorA,fireColorB,finalDensity);

                // Calculate the final blend transparency
                float alpha = finalDensity * skyMask * fireCloudStrength;
                // Blend the original image with the fire cloud color
                vec3 finalColor = mix(originRGB, mix(originRGB,fireCloudColor,0.72), alpha);

                fragColor = vec4(finalColor, origin.a);
            }
        `;

        // Create the post-processing stage instance
        window.fireCloudStage = new Cesium.PostProcessStage({
            name: 'fire_cloud_effect',
            fragmentShader: FireCloudShader,
            uniforms: {
                fireCloudStrength: fireCloudStrength.value,
                time: 0.0
            }
        });

        // requestAnimationFrame drives time to achieve cloud flow animation
        const tick = () => {
            if (window.fireCloudStage) {
                window.fireCloudStage.uniforms.time += 0.016;
                requestAnimationFrame(tick);
            }
        };
        tick();

        // Add the post-processing stage to the rendering pipeline
        window.viewer.scene.postProcessStages.add(window.fireCloudStage);
        isFireCloud.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 {
    position: absolute;
    right: 24px;
    top: 24px;
    z-index: 2;
    display: flex;
    justify-content: start;
    align-items: stretch;
}

.slider-border {
    width: 260px;
    margin-right: 20px;
    position: relative;
    top: -9px;
}

.slider-label {
    font-size: 14px;
}

.btn {
    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;
}

Core Code Explanation:

Multi-layer FBM Cloud Algorithm

By superimposing six layers of noise, the cloud texture becomes rich in detail and clearly stratified, avoiding harsh color blocks. Combined with multiple UV sets at different scales and moving in different directions, it achieves a realistic, disordered cloud flow effect.

Sky Region Mask skyMask

A smooth mask based on the texture's Y-axis coordinate ensures the fire clouds only render in the sky and horizon region, completely avoiding coverage of ground buildings and terrain, making the effect very natural.

Two-Color Sunset Gradient

Adopting an "orange-red + golden-orange" warm-to-cool gradient color scheme, the color is automatically interpolated based on cloud density, perfectly simulating the color temperature changes of a sunset glow.

Summary:

This article has fully implemented the Cesium dynamic flowing fire cloud sunset glow effect. I will continue to output more practical weather effect cases in this Cesium column subsequently.