跪拜 Guibai
← Back to the summary

A Single Fragment Shader Adds a Physically Plausible, Twinkling Night Sky to CesiumJS

Preface

In previous practical sessions, we have already implemented:

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:

night.gif

The static demo effect is shown below:

night_cover2.png

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:

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:

  1. Image Darkening: Use the nightStrength coefficient to globally reduce image brightness, building the basic night environment.
  2. Sky Mask Clipping: Use smoothstep for a smooth transition, precisely locking the sky rendering area.
  3. Random Star Point Generation: A mathematical random function generates high-density, uniformly distributed star positions.
  4. Time Dynamic Interpolation: Update the time variable frame by frame to achieve star breathing twinkling.
  5. 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.