Cesium自定义可调节星空夜景+星星闪烁

前言

在前面的实战中,我们已经实现了:

  • Cesium 黄昏浓度自定义调节(昼夜光影柔化、傍晚氛围)
  • Cesium 雾气浓度可控特效(远近雾化、大气朦胧感)

今天我们继续沿用 Cesium PostProcessStage 后处理着色器 方案,手写一套超高质感、可调节、可开关、真实星空闪烁 的夜景效果。

本案例中Cesium版本为1.141.0

演示动态图效果如下:

演示静态图效果如下:

实现效果:

1. 场景智能分区星空:

通过纹理坐标 Y 轴遮罩计算,严格区分天空区域和地面区域

只在画面上半部分的天空生成星点,山脉、地面完全不会出现星星,完全贴合现实物理逻辑

2. 自然星星呼吸闪烁动画:

基于 sin(time) 正弦周期函数,让每一颗星星拥有独立随机明暗节奏

星星不是统一闪烁,而是零零散散、忽明忽暗、微弱呼吸

3. 可交互式夜景浓度调节

延续我专栏统一的参数化可控思想(和之前两篇的雾效、黄昏特效逻辑一致)。

通过滑块实时修改 nightStrength 数值,可以:

  • 微调夜空昏暗程度
  • 控制星星整体亮度浓度
  • 实现「微夜色 → 深夜纯黑」无缝过渡

4. 冷蓝色深夜夜空基调:

普通夜景只是简单压暗画面,整体发灰、发黑,没有夜空质感。

本次效果叠加了低饱和冷蓝夜空底色,模拟真实深夜大气色调,画面通透、深邃、不发死,夜景层次感极强。

核心实现原理极简梳理:

  1. 画面压暗:通过 nightStrength 系数整体降低画面亮度,构建夜晚基础环境。
  2. 天空遮罩裁切:使用 smoothstep 平滑过渡,精准锁定天空渲染区域。
  3. 随机星点生成:数学随机函数生成高密度、均匀分布的星点位置。
  4. 时间动态插值:逐帧更新时间变量,实现星星呼吸闪烁。
  5. 夜空色调混合:叠加冷蓝底色,提升夜景通透质感。

完整代码

1. template 结构:

xml 复制代码
<template>
    <div class="main">

        <!-- 地球容器 -->
        <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;">夜景昏暗程度:{{ Number(nightStrength).toFixed(2) }}</div>

                <!-- 夜景强度滑块容器,用于控制夜景强度,仅开启星空夜景效果时有效 -->
                <el-slider
                    class="night-slider"
                    v-model.number="nightStrength"
                    :min="0"
                    :max="0.8"
                    :step="0.01"
                    @input="updateNightStrength"
                />
                
            </div>

            <!-- 镜头复位按钮:一键飞回到初始视角 -->
            <el-button type="primary" size="default" class="btn" @click="flyTo">初始位置</el-button>

            <!-- 星空夜景开关按钮,切换开启/关闭状态变量isNightSky,根据isNightSky状态动态修改按钮文字 -->
            <el-button type="primary" size="default" class="btn" @click="nightControl">
                {{ isNightSky ? '关闭星空夜景' : '开启星空夜景' }}
            </el-button>
        </div>

        <!-- 加载提示文字,地图未初始化完成时显示 -->
        <div class="loading" v-if="!isLoading">Loading...</div>

    </div>
</template>

2. script 代码:

ini 复制代码
<script setup>
import { onMounted, nextTick, ref, onUnmounted } from 'vue';
import { token } from '../../utils/common.js';

// 星空夜景开关标识 true=开启夜景,false=关闭夜景
const isNightSky = ref(false);

// 夜景昏暗强度默认值0.8,滑块范围0~0.8
const nightStrength = ref(0.8);

// 地图加载完成标识 false=加载中 true=加载完毕
const isLoading = ref(false);

let myMar = null;

onUnmounted(() => {
    // 销毁星空后处理渲染管线
    if (isNightSky.value && window.nightStage) {
        window.viewer.scene.postProcessStages.remove(window.nightStage);
        window.nightStage = null;
        isNightSky.value = false;
    }

    // 销毁Cesium三维场景实例,释放WebGL、瓦片资源
    if (window.viewer) {
        window.viewer.destroy();
        window.viewer = null;
    }

    if (myMar) {
        clearTimeout(myMar);
        myMar = null;
    }
});

// 组件挂载后:初始化地图
onMounted(() => {
    nextTick(() => {
        initMap();
    });
});

// 初始化 Cesium 地图的方法
const initMap = async () => {
	
    // 设置 Cesium Ion 的token(这里替换成您的Cesium Ion token)
    Cesium.Ion.defaultAccessToken = token;

    // 设置默认视角范围(中国区域)
    Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);

    // 异步加载Cesium官方全球地形服务
    const terrainProvider = await Cesium.createWorldTerrainAsync({
        requestWaterMask: true,
        requestVertexNormals: true  // 启地形顶点法线,光影明暗更立体
    });

    // 创建 Viewer 实例
    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 上下文配置
            webgl: {
                powerPreference: "high-performance",  // 高性能模式
                preserveDrawingBuffer: false  // 不保留绘图缓冲(节省内存)
            }
        }
    });

    // 设置模拟时间(可选,用于光影效果)
    Cesium.JulianDate.fromDate(new Date('2026/05/02 23:00:00'));

    myMar = setTimeout(() => {
        isLoading.value = true;
        flyTo();
    }, 3000);
	
};

// 飞往初始视角
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  // 飞行时长6秒
    });
};

// 滑块拖动实时更新夜景昏暗强度
const updateNightStrength = (val) => {
    if (window.nightStage) {  // 校验夜景处理实例是否存在
        // 将滑块数值传递给着色器uniform变量,实时更新画面夜晚浓度
        window.nightStage.uniforms.nightStrength = Number(val);
    }
};

/**
 * 星空夜景开关核心逻辑
 * 关闭:移除后处理管线、销毁着色器实例
 * 开启:创建星空片元着色器,注册到场景后处理渲染管线
 */
const nightControl = () => {
    if (isNightSky.value) {
        // 关闭星空夜景:从渲染管线移除夜景滤镜,释放WebGL资源
        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;

                // 随机噪声函数,生成星星点位
                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;

                    // 全局夜晚压暗系数
                    float darkRate = 1.0 - nightStrength * 0.65;
                    vec3 nightBase = rawRGB * darkRate;

                    // 仅在画面上半部分(天空)生成星星
                    float skyMask = smoothstep(0.18, 1.0, v_textureCoordinates.y);
                    vec2 starUV = v_textureCoordinates * 1200.0;
                    float randVal = random(floor(starUV));

                    // 星星阈值:只保留少量随机白点作为星星
                    float starMask = step(0.997, randVal);
                    // 正弦时间实现明暗闪烁
                    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;

                    // 夜空冷蓝底色混合
                    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);
                }
            `;

            // 实例化后处理渲染阶段,挂载夜景着色器与全局变量
            window.nightStage = new Cesium.PostProcessStage({
                name: 'night_star_effect',  // 后处理唯一标识名称
                fragmentShader: NightSkyShader,  // 绑定星空片元着色器
                uniforms: {
                    nightStrength: nightStrength.value,  // 初始化夜晚强度数值
                    time: 0.0  // 动画时间初始值
                }
            });

            /**
             * 逐帧动画循环,持续更新time变量驱动星星闪烁
             * 关闭夜景后自动停止循环,节省性能
             */
            const tick = () => {
                if (window.nightStage) {
                    window.nightStage.uniforms.time += 0.016;
                    requestAnimationFrame(tick);
                }
            };
            tick();

            // 将星空夜景滤镜加入场景后处理管线,立即生效
            window.viewer.scene.postProcessStages.add(window.nightStage);
            // 标记夜景状态为开启
            isNightSky.value = true;
    }
};
</script>

3. css样式代码:

css 复制代码
* {
    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;
}

总结:

本文通过原生着色器算法,真正模拟了物理级别的夜空表现:分区天空、动态闪烁、可控浓度、色调真实。

相关推荐
WindfallSheng3 小时前
从Vibe Coding到Spec Coding:一套可落地的AI-SDD企业级研发实战工程
javascript·vue.js
我有满天星辰4 小时前
Vue 指令完全指南:从 Vue 2 到 Vue 3 的演进与实战
前端·javascript·vue.js
我有满天星辰6 小时前
Vue 3 响应式双雄:ref 与 reactive 完全指南
前端·javascript·vue.js
leoZ23117 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
荒诞英雄21 小时前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro21 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte1 天前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅1 天前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七1 天前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript