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:
Effect Highlights:
- Realistic sunset hues: Orange-red to golden-orange gradients simulate sunset fire clouds
- Layered cloud flow: Multiple layers of FBM fractal noise superimposed, with clouds drifting slowly and dynamically
- Adjustable parameters: A slider controls the intensity of the fire cloud effect in real time
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:
- Multiple noise layers superimposed to simulate variations in cloud density
- Different UV scaling combined with time offsets to create cloud flow animation
- A Y-axis mask ensures the effect only applies to the sky region
- Orange-red and golden-yellow dual-color blending simulates the color temperature of a sunset
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.