A Single Fragment Shader Adds a Physically Plausible, Twinkling Night Sky to CesiumJS
Preface
In previous practical sessions, we have already implemented:
- Cesium Customizable Twilight Concentration (day-night light softening, evening atmosphere)
- Cesium Controllable Fog Density Effect (near-far fogging, atmospheric haziness)
Today, we continue using the Cesium PostProcessStage post-processing shader approach to hand-write a set of ultra-high-quality, adjustable, switchable, realistic starry twinkling night scene effects.
The Cesium version used in this case is 1.141.0
The dynamic demo effect is shown below:
The static demo effect is shown below:
Implemented Effects:
1. Scene-Intelligent Zoned Starry Sky:
Through texture coordinate Y-axis masking calculation, the sky area and ground area are strictly distinguished.
Stars are only generated in the sky on the upper half of the screen; mountains and ground will never show stars, fully conforming to real-world physical logic.
2. Natural Star Breathing Twinkle Animation:
Based on the sin(time) sine periodic function, each star is given an independent random brightness rhythm.
Stars do not twinkle uniformly but are scattered, flickering, dimly breathing.
3. Interactive Night Scene Density Adjustment
Continuing the parameterized controllable philosophy of my column (consistent with the logic of the previous two fog and twilight effects).
By modifying the nightStrength value in real-time via a slider, you can:
- Fine-tune the darkness of the night sky
- Control the overall brightness density of the stars
- Achieve a seamless transition from 'light night' to 'deep night pure black'
4. Cool Blue Deep Night Sky Tone:
Ordinary night scenes simply darken the image, making it overall grayish or black without a night sky texture.
This effect overlays a low-saturation cool blue night sky base color, simulating the real atmospheric tone of the deep night. The picture is transparent, profound, and not dead-black, with a strong sense of night scene layering.
Simplified Core Implementation Principle:
- Image Darkening: Use the nightStrength coefficient to globally reduce image brightness, building the basic night environment.
- Sky Mask Clipping: Use smoothstep for a smooth transition, precisely locking the sky rendering area.
- Random Star Point Generation: A mathematical random function generates high-density, uniformly distributed star positions.
- Time Dynamic Interpolation: Update the time variable frame by frame to achieve star breathing twinkling.
- Night Sky Tone Blending: Overlay a cool blue base color to enhance the transparent texture of the night scene.
Complete Code
1. Template Structure:
<template>
<div class="main">
<!-- Earth Container -->
<div class="content" ref="content" id="earth"></div>
<div class="btn-border" v-if="isLoading">
<div class="slider-border" v-if="isNightSky">
<div class="slider-label" style="color:#FFFFFF;">Night Darkness Level: {{ Number(nightStrength).toFixed(2) }}</div>
<!-- Night intensity slider container, used to control night intensity, only effective when the starry night effect is enabled -->
<el-slider
class="night-slider"
v-model.number="nightStrength"
:min="0"
:max="0.8"
:step="0.01"
@input="updateNightStrength"
/>
</div>
<!-- Camera Reset Button: Fly back to the initial viewpoint with one click -->
<el-button type="primary" size="default" class="btn" @click="flyTo">Initial Position</el-button>
<!-- Starry Night Toggle Button, switches the isNightSky state variable, dynamically changes button text based on isNightSky state -->
<el-button type="primary" size="default" class="btn" @click="nightControl">
{{ isNightSky ? 'Close Starry Night' : 'Open Starry Night' }}
</el-button>
</div>
<!-- Loading prompt text, displayed when the map is not yet initialized -->
<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';
// Starry night toggle flag true=night enabled, false=night disabled
const isNightSky = ref(false);
// Night darkness intensity default value 0.8, slider range 0~0.8
const nightStrength = ref(0.8);
// Map loading complete flag false=loading true=loaded
const isLoading = ref(false);
let myMar = null;
onUnmounted(() => {
// Destroy the starry night post-processing render pipeline
if (isNightSky.value && window.nightStage) {
window.viewer.scene.postProcessStages.remove(window.nightStage);
window.nightStage = null;
isNightSky.value = false;
}
// Destroy the Cesium 3D scene instance, release WebGL and tile resources
if (window.viewer) {
window.viewer.destroy();
window.viewer = null;
}
if (myMar) {
clearTimeout(myMar);
myMar = null;
}
});
// After component mount: Initialize the map
onMounted(() => {
nextTick(() => {
initMap();
});
});
// Method to initialize the Cesium map
const initMap = async () => {
// Set the Cesium Ion token (replace this with your Cesium Ion token)
Cesium.Ion.defaultAccessToken = token;
// Set the default view range (China region)
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);
// Asynchronously load the official Cesium global terrain service
const terrainProvider = await Cesium.createWorldTerrainAsync({
requestWaterMask: true,
requestVertexNormals: true // Enable terrain vertex normals for more three-dimensional light and shadow
});
// Create Viewer instance
window.viewer = new Cesium.Viewer('earth', {
terrainProvider: terrainProvider, // Bind global terrain
animation: false, // Disable animation widget
timeline: false, // Disable timeline
infoBox: false, // Disable info box
geocoder: false, // Disable geocoder search
homeButton: false, // Disable home button
sceneModePicker: false, // Disable scene mode picker
baseLayerPicker: false, // Disable base layer picker
navigationHelpButton: false, // Disable navigation help
fullscreenButton: false, // Disable fullscreen button
selectionIndicator: false, // Disable selection indicator
shouldAnimate: false, // Disable auto-play animation
contextOptions: { // WebGL context configuration
webgl: {
powerPreference: "high-performance", // High-performance mode
preserveDrawingBuffer: false // Do not preserve drawing buffer (save memory)
}
}
});
// Set simulation time (optional, for lighting effects)
Cesium.JulianDate.fromDate(new Date('2026/05/02 23:00:00'));
myMar = setTimeout(() => {
isLoading.value = true;
flyTo();
}, 3000);
};
// Fly to the initial viewpoint
const flyTo = () => {
window.viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(117.66293312354773, 26.00085216052459, 1796.8781247739746),
orientation: {
heading: Cesium.Math.toRadians(38.280907385928664), // Heading angle
pitch: Cesium.Math.toRadians(-4.0671391165843245), // Pitch angle
roll: Cesium.Math.toRadians(0.0009439239381974838) // Roll angle
},
duration: 6 // Flight duration 6 seconds
});
};
// Slider drag updates night darkness intensity in real-time
const updateNightStrength = (val) => {
if (window.nightStage) { // Verify if the night processing instance exists
// Pass the slider value to the shader uniform variable, update the night density on screen in real-time
window.nightStage.uniforms.nightStrength = Number(val);
}
};
/**
* Starry night toggle core logic
* Close: Remove post-processing pipeline, destroy shader instance
* Open: Create starry fragment shader, register it to the scene post-processing render pipeline
*/
const nightControl = () => {
if (isNightSky.value) {
// Close starry night: Remove night filter from render pipeline, release WebGL resources
window.viewer.scene.postProcessStages.remove(window.nightStage);
window.nightStage = null;
isNightSky.value = false;
} else {
const NightSkyShader = `
uniform sampler2D colorTexture;
uniform float nightStrength;
uniform float time;
in vec2 v_textureCoordinates;
out vec4 fragColor;
// Random noise function, generates star positions
float random(vec2 uv) {
return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453);
}
void main(void) {
vec4 originColor = texture(colorTexture, v_textureCoordinates);
vec3 rawRGB = originColor.rgb;
// Global night darkening coefficient
float darkRate = 1.0 - nightStrength * 0.65;
vec3 nightBase = rawRGB * darkRate;
// Only generate stars in the upper half of the screen (sky)
float skyMask = smoothstep(0.18, 1.0, v_textureCoordinates.y);
vec2 starUV = v_textureCoordinates * 1200.0;
float randVal = random(floor(starUV));
// Star threshold: only keep a small number of random white points as stars
float starMask = step(0.997, randVal);
// Sine time achieves brightness twinkling
float twinkle = sin(time * 3.2 + randVal * 20.0) * 0.45 + 0.55;
vec3 starColor = vec3(1.0, 1.0, 1.0) * starMask * twinkle * skyMask * nightStrength;
// Night sky cool blue base color blending
vec3 nightTint = vec3(0.04, 0.06, 0.12);
vec3 mixNight = mix(nightBase, nightTint, nightStrength * 0.38);
vec3 finalRGB = mixNight + starColor;
fragColor = vec4(finalRGB, originColor.a);
}
`;
// Instantiate post-processing render stage, mount night shader and global variables
window.nightStage = new Cesium.PostProcessStage({
name: 'night_star_effect', // Unique post-processing identifier name
fragmentShader: NightSkyShader, // Bind starry fragment shader
uniforms: {
nightStrength: nightStrength.value, // Initialize night intensity value
time: 0.0 // Animation time initial value
}
});
/**
* Frame-by-frame animation loop, continuously updates the time variable to drive star twinkling
* Automatically stops the loop when night is turned off, saving performance
*/
const tick = () => {
if (window.nightStage) {
window.nightStage.uniforms.time += 0.016;
requestAnimationFrame(tick);
}
};
tick();
// Add the starry night filter to the scene post-processing pipeline, takes effect immediately
window.viewer.scene.postProcessStages.add(window.nightStage);
// Mark night state as enabled
isNightSky.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;
}
.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;
}
.btn-border {
position: absolute;
right: 24px;
top: 24px;
z-index: 2;
display: flex;
justify-content: start;
align-items: stretch;
}
.btn {
margin-left: 20px;
cursor: pointer;
}
.slider-border {
width: 260px;
margin-right: 20px;
position: relative;
top: -9px;
}
.slider-label {
font-size: 14px;
}
Summary:
This article uses native shader algorithms to truly simulate a physics-level night sky performance: zoned sky, dynamic twinkling, controllable density, and realistic tones.